Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #271 (2020-10-09 17:05:17)

[feature] Network Policy Resource
Adds support for managing Snowflake Network Policy objects via the new `snowflake_network_policy` resource type.

## Test Plan
<!-- detail ways in which this PR has been tested or needs to be tested -->
* [x] acceptance tests
<!-- add more below if you think they are relevant -->
* [x] tested built provider locally with trial Snowflake account

## References
<!-- issues documentation links, etc  -->
* Issue: https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/221
* https://docs.snowflake.com/en/sql-reference/sql/create-network-policy.html

ryanking: Note that I am seeing some acceptance test failures, running `make test-acceptance` locally -

```
=== RUN   TestAccNetworkPolicy
    testing.go:683: Step 0 error: errors during apply:
        
        Error: 000606 (57P03): No active warehouse selected in the current session.  Select an active warehouse with the 'use warehouse' command.
        
        
          on /var/folders/ly/qwpw0g113mz15651mkc9wd400000gr/T/tf-test836548438/main.tf line 2:
          (source code not available)
        
        
    testing.go:744: Error destroying resource! WARNING: Dangling resources
        may exist. The full state and error is shown below.
        
        Error: errors during refresh: 000606 (57P03): No active warehouse selected in the current session.  Select an active warehouse with the 'use warehouse' command.
        
        
        State: <nil>
```

It seems that the select query needs a warehouse, which means we would need to add that to the provider configuration. In the past we've mostly avoided that and instead dealt with the the annoyances of working with just `show` and `describe`, which don't need a warehouse.
grahamplace: > Note that I am seeing some acceptance test failures, running `make test-acceptance` locally -
> 
> ```
> === RUN   TestAccNetworkPolicy
>     testing.go:683: Step 0 error: errors during apply:
>         
>         Error: 000606 (57P03): No active warehouse selected in the current session.  Select an active warehouse with the 'use warehouse' command.
>         
>         
>           on /var/folders/ly/qwpw0g113mz15651mkc9wd400000gr/T/tf-test836548438/main.tf line 2:
>           (source code not available)
>         
>         
>     testing.go:744: Error destroying resource! WARNING: Dangling resources
>         may exist. The full state and error is shown below.
>         
>         Error: errors during refresh: 000606 (57P03): No active warehouse selected in the current session.  Select an active warehouse with the 'use warehouse' command.
>         
>         
>         State: <nil>
> ```
> 
> It seems that the select query needs a warehouse, which means we would need to add that to the provider configuration. In the past we've mostly avoided that and instead dealt with the the annoyances of working with just `show` and `describe`, which don't need a warehouse.

Similar to my reply on your other comment — the issue here is that Snowflake's implementation of Network Policies is weird/somewhat different than other object types. 

More specifically in this case, I think we _have_ to do the `RESULT_SCAN` approach I used (which is the cause of acceptance tests user requiring a warehouse). This is because there is no way to `SHOW`/`DESC` the information about a _single_ Network Policy in Snowflake SQL:

- [DESC](https://docs.snowflake.com/en/sql-reference/sql/desc-network-policy.html) is limited to a single Network Policy, but it only returns the IP address lists: 
![image](https://user-images.githubusercontent.com/19509185/95787741-faf23a00-0c8e-11eb-8456-13c988b6a9b4.png)

- [SHOW](https://docs.snowflake.com/en/sql-reference/sql/show-network-policies.html) _only_ allows `SHOW NETWORK POLICIES` — for some reason, unlike other object types, the output cannot be limited to a single Network Policy object via `LIKE`. Adding a `LIKE` does not limit the returned data:
![image](https://user-images.githubusercontent.com/19509185/95788014-8bc91580-0c8f-11eb-8d5a-64feee88ca0d.png)

Thus, in order to get the current `comment` on the Network Policy in question, the `SHOW` output has to be cut down to just `name = "mypolicy"` via a second query that uses `RESULT_SCAN('original_query_id')`


I worked around this by setting `DEFAULT_WAREHOUSE` on the Travis user. Not ideal obviously, but I don't have other ideas for how to implement `ReadNetworkPolicy` without the `RESULT_SCAN` approach given limitations outlined above. Open to ideas!
grahamplace: ^ while writing that I realized that I can probably just refactor to do the SHOW/DESC post-processing in Go instead of in Snowflake SQL, thus bypassing the `RESULT_SCAN` and the need for a Warehouse

Still posted as it's useful context, but I'll take a stab at that refactor now
aidan-melen: thanks for working on this feature @grahamplace and @ryanking!
grahamplace: Thanks for the review @alldoami! Per Ryan's [suggestion](https://github.com/chanzuckerberg/terraform-provider-snowflake/pull/271#discussion_r504157297), I still need to refactor this to move the attachment into a separate resource. That work is on my todo list for today, so hopefully I can put that change up and tag you both for another look 
grahamplace: Done! Ready for another look @ryanking @alldoami 🙂  Would appreciate input on this bit:
https://github.com/chanzuckerberg/terraform-provider-snowflake/pull/271#discussion_r508133483
grahamplace: > I am currently getting acceptance test failures too:
> 
> ```
> pkg/resources/network_policy_attachment_acceptance_test.go:12:6: TestAccNetworkPolicyAttachment redeclared in this block
> 	previous declaration at pkg/resources/network_policy_acceptance_test.go:12:40
> pkg/resources/network_policy_attachment_acceptance_test.go:40:6: networkPolicyAttachmentConfig redeclared in this block
> 	previous declaration at pkg/resources/network_policy_acceptance_test.go:40:55
> ```

Ah oops, I accidentally overwrote `network_policy_acceptance_test.go` with `network_policy_attachment_acceptance_test.go`, should be corrected now

Patch:
diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md
new file mode 100644
index 00000000..14e7996e
--- /dev/null
+++ b/docs/resources/network_policy.md
@@ -0,0 +1,13 @@
+
+# snowflake_network_policy
+
+<!-- These docs are auto-generated by code in ./docgen, run by with make docs. Manual edits will be overwritten. -->
+
+## properties
+
+|      NAME       |  TYPE  |                                                                         DESCRIPTION                                                                          | OPTIONAL | REQUIRED  | COMPUTED | DEFAULT |
+|-----------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|----------|---------|
+| allowed_ip_list | set    | Specifies one or more IPv4 addresses (CIDR notation) that are allowed access to your Snowflake account                                                       | false    | true      | false    |         |
+| blocked_ip_list | set    | Specifies one or more IPv4 addresses (CIDR notation) that are denied access to your Snowflake account<br><br>**Do not** add `0.0.0.0/0` to `blocked_ip_list` | true     | false     | false    |         |
+| comment         | string | Specifies a comment for the network policy.                                                                                                                  | true     | false     | false    |         |
+| name            | string | Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.                                      | false    | true      | false    |         |
diff --git a/docs/resources/network_policy_attachment.md b/docs/resources/network_policy_attachment.md
new file mode 100644
index 00000000..439273e7
--- /dev/null
+++ b/docs/resources/network_policy_attachment.md
@@ -0,0 +1,12 @@
+
+# snowflake_network_policy_attachment
+
+<!-- These docs are auto-generated by code in ./docgen, run by with make docs. Manual edits will be overwritten. -->
+
+## properties
+
+|        NAME         |  TYPE  |                                                                                                                                                                                                                                                                                                       DESCRIPTION                                                                                                                                                                                                                                                                                                        | OPTIONAL | REQUIRED  | COMPUTED | DEFAULT |
+|---------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|----------|---------|
+| network_policy_name | string | Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | false    | true      | false    |         |
+| set_for_account     | bool   | Specifies whether the network policy should be applied globally to your Snowflake account<br><br>**Note:** The Snowflake user running `terraform apply` must be on an IP address allowed by the network policy to set that policy globally on the Snowflake account.<br><br>Additionally, a Snowflake account can only have one network policy set globally at any given time. This resource does not enforce one-policy-per-account, it is the user's responsibility to enforce this. If multiple network policy resources have `set_for_account: true`, the final policy set on the account will be non-deterministic. | true     | false     | false    | false   |
+| users               | set    | Specifies which users the network policy should be attached to                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | true     | false     | false    |         |
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 3f81ba22..ddb45693 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -68,30 +68,32 @@ func Provider() *schema.Provider {
 			},
 		},
 		ResourcesMap: map[string]*schema.Resource{
-			"snowflake_account_grant":          resources.AccountGrant(),
-			"snowflake_database":               resources.Database(),
-			"snowflake_database_grant":         resources.DatabaseGrant(),
-			"snowflake_integration_grant":      resources.IntegrationGrant(),
-			"snowflake_managed_account":        resources.ManagedAccount(),
-			"snowflake_pipe":                   resources.Pipe(),
-			"snowflake_resource_monitor":       resources.ResourceMonitor(),
-			"snowflake_resource_monitor_grant": resources.ResourceMonitorGrant(),
-			"snowflake_role":                   resources.Role(),
-			"snowflake_role_grants":            resources.RoleGrants(),
-			"snowflake_schema":                 resources.Schema(),
-			"snowflake_schema_grant":           resources.SchemaGrant(),
-			"snowflake_share":                  resources.Share(),
-			"snowflake_stage":                  resources.Stage(),
-			"snowflake_stage_grant":            resources.StageGrant(),
-			"snowflake_storage_integration":    resources.StorageIntegration(),
-			"snowflake_user":                   resources.User(),
-			"snowflake_view":                   resources.View(),
-			"snowflake_view_grant":             resources.ViewGrant(),
-			"snowflake_task":                   resources.Task(),
-			"snowflake_table":                  resources.Table(),
-			"snowflake_table_grant":            resources.TableGrant(),
-			"snowflake_warehouse":              resources.Warehouse(),
-			"snowflake_warehouse_grant":        resources.WarehouseGrant(),
+			"snowflake_account_grant":             resources.AccountGrant(),
+			"snowflake_database":                  resources.Database(),
+			"snowflake_database_grant":            resources.DatabaseGrant(),
+			"snowflake_integration_grant":         resources.IntegrationGrant(),
+			"snowflake_managed_account":           resources.ManagedAccount(),
+			"snowflake_network_policy":            resources.NetworkPolicy(),
+			"snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(),
+			"snowflake_pipe":                      resources.Pipe(),
+			"snowflake_resource_monitor":          resources.ResourceMonitor(),
+			"snowflake_resource_monitor_grant":    resources.ResourceMonitorGrant(),
+			"snowflake_role":                      resources.Role(),
+			"snowflake_role_grants":               resources.RoleGrants(),
+			"snowflake_schema":                    resources.Schema(),
+			"snowflake_schema_grant":              resources.SchemaGrant(),
+			"snowflake_share":                     resources.Share(),
+			"snowflake_stage":                     resources.Stage(),
+			"snowflake_stage_grant":               resources.StageGrant(),
+			"snowflake_storage_integration":       resources.StorageIntegration(),
+			"snowflake_user":                      resources.User(),
+			"snowflake_view":                      resources.View(),
+			"snowflake_view_grant":                resources.ViewGrant(),
+			"snowflake_task":                      resources.Task(),
+			"snowflake_table":                     resources.Table(),
+			"snowflake_table_grant":               resources.TableGrant(),
+			"snowflake_warehouse":                 resources.Warehouse(),
+			"snowflake_warehouse_grant":           resources.WarehouseGrant(),
 		},
 		DataSourcesMap: map[string]*schema.Resource{
 			"snowflake_system_get_aws_sns_iam_policy": datasources.SystemGetAWSSNSIAMPolicy(),
diff --git a/pkg/resources/network_policy.go b/pkg/resources/network_policy.go
new file mode 100644
index 00000000..e0af1d81
--- /dev/null
+++ b/pkg/resources/network_policy.go
@@ -0,0 +1,224 @@
+package resources
+
+import (
+	"database/sql"
+	"fmt"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/pkg/errors"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+)
+
+var networkPolicySchema = map[string]*schema.Schema{
+	"name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.",
+		ForceNew:    true,
+	},
+	"allowed_ip_list": {
+		Type:        schema.TypeSet,
+		Elem:        &schema.Schema{Type: schema.TypeString},
+		Required:    true,
+		Description: "Specifies one or more IPv4 addresses (CIDR notation) that are allowed access to your Snowflake account",
+	},
+	// TODO: Add a ValidationFunc to ensure 0.0.0.0/0 is not in blocked_ip_list
+	// See: https://docs.snowflake.com/en/user-guide/network-policies.html#create-an-account-level-network-policy
+	"blocked_ip_list": {
+		Type:        schema.TypeSet,
+		Elem:        &schema.Schema{Type: schema.TypeString},
+		Optional:    true,
+		Description: "Specifies one or more IPv4 addresses (CIDR notation) that are denied access to your Snowflake account<br><br>**Do not** add `0.0.0.0/0` to `blocked_ip_list`",
+	},
+	"comment": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "Specifies a comment for the network policy.",
+	},
+}
+
+// NetworkPolicy returns a pointer to the resource representing a network policy
+func NetworkPolicy() *schema.Resource {
+	return &schema.Resource{
+		Create: CreateNetworkPolicy,
+		Read:   ReadNetworkPolicy,
+		Update: UpdateNetworkPolicy,
+		Delete: DeleteNetworkPolicy,
+
+		Schema: networkPolicySchema,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
+	}
+}
+
+// CreateNetworkPolicy implements schema.CreateFunc
+func CreateNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := data.Get("name").(string)
+	builder := snowflake.NetworkPolicy(name)
+
+	// Set optionals
+	if v, ok := data.GetOk("comment"); ok {
+		builder.WithComment(v.(string))
+	}
+
+	if v, ok := data.GetOk("allowed_ip_list"); ok {
+		builder.WithAllowedIpList(expandStringList(v.(*schema.Set).List()))
+	}
+
+	if v, ok := data.GetOk("blocked_ip_list"); ok {
+		builder.WithBlockedIpList(expandStringList(v.(*schema.Set).List()))
+	}
+
+	stmt := builder.Create()
+	err := snowflake.Exec(db, stmt)
+	if err != nil {
+		return errors.Wrapf(err, "error creating network policy %v", name)
+	}
+	data.SetId(name)
+
+	return ReadNetworkPolicy(data, meta)
+}
+
+// ReadNetworkPolicy implements schema.ReadFunc
+func ReadNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	policyName := data.Id()
+
+	builder := snowflake.NetworkPolicy(policyName)
+
+	// There is no way to SHOW a single Network Policy, so we have to read *all* network policies and filter in memory
+	showSql := builder.ShowAllNetworkPolicies()
+
+	rows, err := snowflake.Query(db, showSql)
+	if err != nil {
+		return err
+	}
+
+	allPolicies, err := snowflake.ScanNetworkPolicies(rows)
+	if err != nil {
+		return err
+	}
+
+	var s *snowflake.NetworkPolicyStruct
+	for _, value := range allPolicies {
+		if value.Name.String == policyName {
+			s = value
+		}
+	}
+
+	descSql := builder.Describe()
+	rows, err = snowflake.Query(db, descSql)
+	if err != nil {
+		return err
+	}
+
+	err = data.Set("name", s.Name.String)
+	if err != nil {
+		return err
+	}
+
+	err = data.Set("comment", s.Comment.String)
+	if err != nil {
+		return err
+	}
+
+	var (
+		name  string
+		value string
+	)
+	for rows.Next() {
+		err := rows.Scan(&name, &value)
+		if err != nil {
+			return err
+		}
+
+		if name == "ALLOWED_IP_LIST" {
+			err = data.Set("allowed_ip_list", strings.Split(value, ","))
+			if err != nil {
+				return err
+			}
+		} else if name == "BLOCKED_IP_LIST" {
+			err = data.Set("blocked_ip_list", strings.Split(value, ","))
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return err
+}
+
+// UpdateNetworkPolicy implements schema.UpdateFunc
+func UpdateNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := data.Id()
+	builder := snowflake.NetworkPolicy(name)
+
+	if data.HasChange("comment") {
+		_, comment := data.GetChange("comment")
+
+		if c := comment.(string); c == "" {
+			q := builder.RemoveComment()
+			err := snowflake.Exec(db, q)
+			if err != nil {
+				return errors.Wrapf(err, "error unsetting comment for network policy %v", name)
+			}
+		} else {
+			q := builder.ChangeComment(c)
+			err := snowflake.Exec(db, q)
+			if err != nil {
+				return errors.Wrapf(err, "error updating comment for network policy %v", name)
+			}
+		}
+	}
+
+	if data.HasChange("allowed_ip_list") {
+		newIps := ipChangeParser(data, "allowed_ip_list")
+		q := builder.ChangeIpList("ALLOWED", newIps)
+		err := snowflake.Exec(db, q)
+		if err != nil {
+			return errors.Wrapf(err, "error updating ALLOWED_IP_LIST for network policy %v", name)
+		}
+	}
+
+	if data.HasChange("blocked_ip_list") {
+		newIps := ipChangeParser(data, "blocked_ip_list")
+		q := builder.ChangeIpList("BLOCKED", newIps)
+		err := snowflake.Exec(db, q)
+		if err != nil {
+			return errors.Wrapf(err, "error updating BLOCKED_IP_LIST for network policy %v", name)
+		}
+	}
+
+	return ReadNetworkPolicy(data, meta)
+}
+
+// DeleteNetworkPolicy implements schema.DeleteFunc
+func DeleteNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := data.Id()
+
+	dropSql := snowflake.NetworkPolicy(name).Drop()
+	err := snowflake.Exec(db, dropSql)
+	if err != nil {
+		return errors.Wrapf(err, "error deleting network policy %v", name)
+	}
+
+	data.SetId("")
+	return nil
+}
+
+// ipChangeParser is a helper function to parse a given ip list change from ResourceData
+func ipChangeParser(data *schema.ResourceData, key string) []string {
+	_, ipChangeSet := data.GetChange(key)
+	ipList := ipChangeSet.(*schema.Set).List()
+	newIps := make([]string, len(ipList))
+	for idx, value := range ipList {
+		newIps[idx] = fmt.Sprintf("%v", value)
+	}
+	return newIps
+}
diff --git a/pkg/resources/network_policy_attachment.go b/pkg/resources/network_policy_attachment.go
new file mode 100644
index 00000000..b50a7dd8
--- /dev/null
+++ b/pkg/resources/network_policy_attachment.go
@@ -0,0 +1,253 @@
+package resources
+
+import (
+	"database/sql"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/pkg/errors"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+)
+
+var networkPolicyAttachmentSchema = map[string]*schema.Schema{
+	"network_policy_name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.",
+		ForceNew:    true,
+	},
+	"set_for_account": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Description: "Specifies whether the network policy should be applied globally to your Snowflake account<br><br>**Note:** The Snowflake user running `terraform apply` must be on an IP address allowed by the network policy to set that policy globally on the Snowflake account.<br><br>Additionally, a Snowflake account can only have one network policy set globally at any given time. This resource does not enforce one-policy-per-account, it is the user's responsibility to enforce this. If multiple network policy resources have `set_for_account: true`, the final policy set on the account will be non-deterministic.",
+		Default:     false,
+	},
+	"users": {
+		Type:        schema.TypeSet,
+		Elem:        &schema.Schema{Type: schema.TypeString},
+		Optional:    true,
+		Description: "Specifies which users the network policy should be attached to",
+	},
+}
+
+// NetworkPolicyAttachment returns a pointer to the resource representing a network policy attachment
+func NetworkPolicyAttachment() *schema.Resource {
+	return &schema.Resource{
+		Create: CreateNetworkPolicyAttachment,
+		Read:   ReadNetworkPolicyAttachment,
+		Update: UpdateNetworkPolicyAttachment,
+		Delete: DeleteNetworkPolicyAttachment,
+
+		Schema: networkPolicyAttachmentSchema,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
+	}
+}
+
+// CreateNetworkPolicyAttachment implements schema.CreateFunc
+func CreateNetworkPolicyAttachment(data *schema.ResourceData, meta interface{}) error {
+	policyName := data.Get("network_policy_name").(string)
+	data.SetId(policyName + "_attachment")
+
+	if data.Get("set_for_account").(bool) {
+		err := setOnAccount(data, meta)
+		if err != nil {
+			return errors.Wrapf(err, "error creating attachment for network policy %v", policyName)
+		}
+	}
+
+	if u, ok := data.GetOk("users"); ok {
+		users := expandStringList(u.(*schema.Set).List())
+
+		err := ensureUserAlterPrivileges(users, meta)
+		if err != nil {
+			return err
+		}
+
+		err = setOnUsers(users, data, meta)
+		if err != nil {
+			return errors.Wrapf(err, "error creating attachment for network policy %v", policyName)
+		}
+	}
+
+	return nil
+}
+
+// ReadNetworkPolicyAttachment implements schema.ReadFunc
+func ReadNetworkPolicyAttachment(data *schema.ResourceData, meta interface{}) error {
+	// HACK: InternalValidate requires Read to be implemented
+	// There is no way of using SHOW/DESC on Network Policies/Users to pull attachment information, so we can't actually Read
+	return nil
+}
+
+// UpdateNetworkPolicyAttachment implements schema.UpdateFunc
+func UpdateNetworkPolicyAttachment(data *schema.ResourceData, meta interface{}) error {
+	if data.HasChange("set_for_account") {
+		oldAcctFlag, newAcctFlag := data.GetChange("set_for_account")
+		if newAcctFlag.(bool) == true {
+			setOnAccount(data, meta)
+		} else if newAcctFlag.(bool) == false && oldAcctFlag == true {
+			unsetOnAccount(data, meta)
+		}
+	}
+
+	if data.HasChange("users") {
+		old, new := data.GetChange("users")
+		oldUsersSet := old.(*schema.Set)
+		newUsersSet := new.(*schema.Set)
+
+		removedUsers := expandStringList(oldUsersSet.Difference(newUsersSet).List())
+		addedUsers := expandStringList(newUsersSet.Difference(oldUsersSet).List())
+
+		err := ensureUserAlterPrivileges(removedUsers, meta)
+		if err != nil {
+			return err
+		}
+
+		err = ensureUserAlterPrivileges(addedUsers, meta)
+		if err != nil {
+			return err
+		}
+
+		for _, user := range removedUsers {
+			unsetOnUser(user, data, meta)
+			if err != nil {
+				return err
+			}
+		}
+
+		for _, user := range addedUsers {
+			setOnUser(user, data, meta)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// DeleteNetworkPolicyAttachment implements schema.DeleteFunc
+func DeleteNetworkPolicyAttachment(data *schema.ResourceData, meta interface{}) error {
+	policyName := data.Get("network_policy_name").(string)
+	data.SetId(policyName + "_attachment")
+
+	err := unsetOnAccount(data, meta)
+	if err != nil {
+		return errors.Wrapf(err, "error deleting attachment for network policy %v", policyName)
+	}
+
+	if u, ok := data.GetOk("users"); ok {
+		users := expandStringList(u.(*schema.Set).List())
+
+		err := ensureUserAlterPrivileges(users, meta)
+		if err != nil {
+			return err
+		}
+
+		err = unsetOnUsers(users, data, meta)
+		if err != nil {
+			return errors.Wrapf(err, "error deleting attachment for network policy %v", policyName)
+		}
+	}
+
+	return nil
+}
+
+// setOnAccount sets the network policy globally for the Snowflake account
+// Note: the ip address of the session executing this SQL must be allowed by the network policy being set
+func setOnAccount(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	policyName := data.Get("network_policy_name").(string)
+
+	acctSql := snowflake.NetworkPolicy(policyName).SetOnAccount()
+
+	err := snowflake.Exec(db, acctSql)
+	if err != nil {
+		return errors.Wrapf(err, "error setting network policy %v on account", policyName)
+	}
+
+	return nil
+}
+
+// setOnAccount unsets the network policy globally for the Snowflake account
+func unsetOnAccount(data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	policyName := data.Get("network_policy_name").(string)
+
+	acctSql := snowflake.NetworkPolicy(policyName).UnsetOnAccount()
+
+	err := snowflake.Exec(db, acctSql)
+	if err != nil {
+		return errors.Wrapf(err, "error unsetting network policy %v on account", policyName)
+	}
+
+	return nil
+}
+
+// setOnUsers sets the network policy for list of users
+func setOnUsers(users []string, data *schema.ResourceData, meta interface{}) error {
+	policyName := data.Get("network_policy_name").(string)
+	for _, user := range users {
+		err := setOnUser(user, data, meta)
+		if err != nil {
+			return errors.Wrapf(err, "error setting network policy %v on user %v", policyName, user)
+		}
+	}
+
+	return nil
+}
+
+// setOnUser sets the network policy for a given user
+func setOnUser(user string, data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	policyName := data.Get("network_policy_name").(string)
+	userSql := snowflake.NetworkPolicy(policyName).SetOnUser(user)
+	err := snowflake.Exec(db, userSql)
+	if err != nil {
+		return errors.Wrapf(err, "error setting network policy %v on user %v", policyName, user)
+	}
+
+	return nil
+}
+
+// unsetOnUsers unsets the network policy for list of users
+func unsetOnUsers(users []string, data *schema.ResourceData, meta interface{}) error {
+	policyName := data.Get("network_policy_name").(string)
+	for _, user := range users {
+		err := unsetOnUser(user, data, meta)
+		if err != nil {
+			return errors.Wrapf(err, "error unsetting network policy %v on user %v", policyName, user)
+		}
+	}
+
+	return nil
+}
+
+// unsetOnUser sets the network policy for a given user
+func unsetOnUser(user string, data *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	policyName := data.Get("network_policy_name").(string)
+	userSql := snowflake.NetworkPolicy(policyName).UnsetOnUser(user)
+	err := snowflake.Exec(db, userSql)
+	if err != nil {
+		return errors.Wrapf(err, "error unsetting network policy %v on user %v", policyName, user)
+	}
+
+	return nil
+}
+
+// ensureUserAlterPrivileges ensures the executing Snowflake user can alter each user in the set of users
+func ensureUserAlterPrivileges(users []string, meta interface{}) error {
+	db := meta.(*sql.DB)
+	for _, user := range users {
+		userDescSql := snowflake.User(user).Describe()
+		err := snowflake.Exec(db, userDescSql)
+		if err != nil {
+			return errors.Wrapf(err, "error altering network policy of user %v", user)
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/snowflake/network_policy.go b/pkg/snowflake/network_policy.go
new file mode 100644
index 00000000..5c514ed1
--- /dev/null
+++ b/pkg/snowflake/network_policy.go
@@ -0,0 +1,145 @@
+package snowflake
+
+import (
+	"database/sql"
+	"fmt"
+	"strings"
+
+	"github.com/jmoiron/sqlx"
+)
+
+// NetworkPolicyBuilder abstracts the creation of SQL queries for a Snowflake Network Policy
+type NetworkPolicyBuilder struct {
+	name          string
+	comment       string
+	allowedIpList string
+	blockedIpList string
+}
+
+// WithComment adds a comment to the NetworkPolicyBuilder
+func (npb *NetworkPolicyBuilder) WithComment(c string) *NetworkPolicyBuilder {
+	npb.comment = EscapeString(c)
+	return npb
+}
+
+// WithAllowedIpList adds an allowedIpList to the NetworkPolicyBuilder
+func (npb *NetworkPolicyBuilder) WithAllowedIpList(allowedIps []string) *NetworkPolicyBuilder {
+	npb.allowedIpList = IpListToString(allowedIps)
+	return npb
+}
+
+// WithBlockedIpList adds a blockedIpList to the NetworkPolicyBuilder
+func (npb *NetworkPolicyBuilder) WithBlockedIpList(blockedIps []string) *NetworkPolicyBuilder {
+	npb.blockedIpList = IpListToString(blockedIps)
+	return npb
+}
+
+// NetworkPolicy returns a pointer to a Builder that abstracts the DDL operations for a network policy.
+//
+// Supported DDL operations are:
+//   - CREATE NETWORK POLICY
+//   - DROP NETWORK POLICY
+//   - SHOW NETWORK POLICIES
+//
+// [Snowflake Reference](https://docs.snowflake.com/en/user-guide/network-policies.html)
+func NetworkPolicy(name string) *NetworkPolicyBuilder {
+	return &NetworkPolicyBuilder{
+		name: name,
+	}
+}
+
+// Create returns the SQL query that will create a network policy.
+func (npb *NetworkPolicyBuilder) Create() string {
+	createSql := fmt.Sprintf(`CREATE NETWORK POLICY "%v" ALLOWED_IP_LIST=%v`, npb.name, npb.allowedIpList)
+	if npb.blockedIpList != "" {
+		createSql = createSql + fmt.Sprintf(" BLOCKED_IP_LIST=%v", npb.blockedIpList)
+	}
+	if npb.comment != "" {
+		createSql = createSql + fmt.Sprintf(` COMMENT="%v"`, npb.comment)
+	}
+
+	return createSql
+}
+
+// Describe returns the SQL query that will describe a network policy
+func (npb *NetworkPolicyBuilder) Describe() string {
+	return fmt.Sprintf(`DESC NETWORK POLICY "%v"`, npb.name)
+}
+
+// ChangeComment returns the SQL query that will update the comment on the network policy.
+func (npb *NetworkPolicyBuilder) ChangeComment(c string) string {
+	return fmt.Sprintf(`ALTER NETWORK POLICY "%v" SET COMMENT = '%v'`, npb.name, EscapeString(c))
+}
+
+// RemoveComment returns the SQL query that will remove the comment on the network policy.
+func (npb *NetworkPolicyBuilder) RemoveComment() string {
+	return fmt.Sprintf(`ALTER NETWORK POLICY "%v" UNSET COMMENT`, npb.name)
+}
+
+// ChangeIpList returns the SQL query that will update the ip list (of the specified listType) on the network policy.
+func (npb *NetworkPolicyBuilder) ChangeIpList(listType string, ips []string) string {
+	return fmt.Sprintf(`ALTER NETWORK POLICY "%v" SET %v_IP_LIST = %v`, npb.name, listType, IpListToString(ips))
+}
+
+// Drop returns the SQL query that will drop a network policy.
+func (npb *NetworkPolicyBuilder) Drop() string {
+	return fmt.Sprintf(`DROP NETWORK POLICY "%v"`, npb.name)
+}
+
+// SetOnAccount returns the SQL query that will set the network policy globally on your Snowflake account
+func (npb *NetworkPolicyBuilder) SetOnAccount() string {
+	return fmt.Sprintf(`ALTER ACCOUNT SET NETWORK_POLICY = "%v"`, npb.name)
+}
+
+// UnsetOnAccount returns the SQL query that will unset the network policy globally on your Snowflake account
+func (npb *NetworkPolicyBuilder) UnsetOnAccount() string {
+	return fmt.Sprintf(`ALTER ACCOUNT UNSET NETWORK_POLICY`)
+}
+
+// SetOnUser returns the SQL query that will set the network policy on a given user
+func (npb *NetworkPolicyBuilder) SetOnUser(u string) string {
+	return fmt.Sprintf(`ALTER USER "%v" SET NETWORK_POLICY = "%v"`, u, npb.name)
+}
+
+// UnsetOnUser returns the SQL query that will unset the network policy of a given user
+func (npb *NetworkPolicyBuilder) UnsetOnUser(u string) string {
+	return fmt.Sprintf(`ALTER USER "%v" UNSET NETWORK_POLICY`, u)
+}
+
+// ShowAllNetworkPolicies returns the SQL query that will SHOW *all* network policies in the Snowflake account
+// Snowflake's implementation of SHOW for network policies does *not* support limiting results with LIKE
+func (npb *NetworkPolicyBuilder) ShowAllNetworkPolicies() string {
+	return fmt.Sprintf(`SHOW NETWORK POLICIES`)
+}
+
+// IpListToString formats a list of IPs into a Snowflake-DDL friendly string, e.g. ('192.168.1.0', '192.168.1.100')
+func IpListToString(ips []string) string {
+	for index, element := range ips {
+		ips[index] = fmt.Sprintf(`'%v'`, element)
+	}
+
+	return fmt.Sprintf("(%v)", strings.Join(ips, ", "))
+}
+
+type NetworkPolicyStruct struct {
+	CreatedOn              sql.NullString `db:"created_on"`
+	Name                   sql.NullString `db:"name"`
+	Comment                sql.NullString `db:"comment"`
+	EntriesInAllowedIpList sql.NullString `db:"entries_in_allowed_ip_list"`
+	EntriesInBlockedIpList sql.NullString `db:"entries_in_blocked_ip_list"`
+}
+
+// ScanNetworkPolicies takes database rows and converts them to a list of NetworkPolicyStruct pointers
+func ScanNetworkPolicies(rows *sqlx.Rows) ([]*NetworkPolicyStruct, error) {
+	var n []*NetworkPolicyStruct
+
+	for rows.Next() {
+		r := &NetworkPolicyStruct{}
+		err := rows.StructScan(r)
+		if err != nil {
+			return nil, err
+		}
+		n = append(n, r)
+	}
+	return n, nil
+}


Test patch:
diff --git a/pkg/resources/network_policy_acceptance_test.go b/pkg/resources/network_policy_acceptance_test.go
new file mode 100644
index 00000000..b3a44cbe
--- /dev/null
+++ b/pkg/resources/network_policy_acceptance_test.go
@@ -0,0 +1,74 @@
+package resources_test
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+)
+
+const (
+	networkPolicyComment = "Created by a Terraform acceptance test"
+)
+
+func TestAccNetworkPolicy(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_NETWORK_POLICY_TESTS"); ok {
+		t.Skip("Skipping TestAccNetworkPolicy")
+	}
+
+	name := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
+
+	resource.Test(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: networkPolicyConfig(name),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "name", name),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "comment", networkPolicyComment),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "allowed_ip_list.#", "2"),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "blocked_ip_list.#", "0"),
+				),
+			},
+			// CHANGE PROPERTIES
+			{
+				Config: networkPolicyConfig2(name),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "name", name),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "comment", networkPolicyComment),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "allowed_ip_list.#", "1"),
+					resource.TestCheckResourceAttr("snowflake_network_policy.test", "blocked_ip_list.#", "1"),
+				),
+			},
+			// IMPORT
+			{
+				ResourceName:      "snowflake_network_policy.test",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
+func networkPolicyConfig(name string) string {
+	return fmt.Sprintf(`
+resource "snowflake_network_policy" "test" {
+	name            = "%v"
+	comment         = "%v"
+	allowed_ip_list = ["192.168.0.100/24", "29.254.123.20"]
+}
+`, name, networkPolicyComment)
+}
+
+func networkPolicyConfig2(name string) string {
+	return fmt.Sprintf(`
+resource "snowflake_network_policy" "test" {
+	name            = "%v"
+	comment         = "%v"
+	allowed_ip_list = ["192.168.0.100/24"]
+	blocked_ip_list = ["192.168.0.101"]
+}
+`, name, networkPolicyComment)
+}
diff --git a/pkg/resources/network_policy_attachment_acceptance_test.go b/pkg/resources/network_policy_attachment_acceptance_test.go
new file mode 100644
index 00000000..16a6348d
--- /dev/null
+++ b/pkg/resources/network_policy_attachment_acceptance_test.go
@@ -0,0 +1,61 @@
+package resources_test
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+)
+
+func TestAccNetworkPolicyAttachment(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_NETWORK_POLICY_TESTS"); ok {
+		t.Skip("Skipping TestAccNetworkPolicyAttachment")
+	}
+
+	policyName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
+
+	resource.Test(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: networkPolicyAttachmentConfig(policyName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_network_policy_attachment.test", "network_policy_name", policyName),
+					resource.TestCheckResourceAttr("snowflake_network_policy_attachment.test", "set_for_account", "false"),
+					resource.TestCheckResourceAttr("snowflake_network_policy_attachment.test", "users.#", "2"),
+				),
+			},
+			// IMPORT
+			{
+				ResourceName:      "snowflake_network_policy_attachment.test",
+				ImportState:       true,
+				ImportStateVerify: false,
+			},
+		},
+	})
+}
+
+func networkPolicyAttachmentConfig(policyName string) string {
+	return fmt.Sprintf(`
+resource "snowflake_user" "test-user1" {
+	name = "test-user1"
+}
+
+resource "snowflake_user" "test-user2" {
+	name = "test-user2"
+}
+
+resource "snowflake_network_policy" "test" {
+	name            = "%v"
+	allowed_ip_list = ["192.168.0.100/24", "29.254.123.20"]
+}
+
+resource "snowflake_network_policy_attachment" "test" {
+	network_policy_name = snowflake_network_policy.test.name
+	set_for_account     = false
+	users               = [snowflake_user.test-user1.name, snowflake_user.test-user2.name]
+}
+`, policyName)
+}
diff --git a/pkg/resources/network_policy_attachment_test.go b/pkg/resources/network_policy_attachment_test.go
new file mode 100644
index 00000000..e7f40a0f
--- /dev/null
+++ b/pkg/resources/network_policy_attachment_test.go
@@ -0,0 +1,63 @@
+package resources_test
+
+import (
+	"database/sql"
+	"testing"
+
+	sqlmock "github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/require"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider"
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources"
+	. "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
+)
+
+func TestNetworkPolicyAttachment(t *testing.T) {
+	r := require.New(t)
+	err := resources.NetworkPolicyAttachment().InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestNetworkPolicyAttachmentCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"network_policy_name": "test-network-policy",
+		"set_for_account":     true,
+		"users":               []interface{}{"test-user"},
+	}
+	d := schema.TestResourceDataRaw(t, resources.NetworkPolicyAttachment().Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^ALTER ACCOUNT SET NETWORK_POLICY = "test-network-policy"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		mock.ExpectExec(`^DESCRIBE USER "test-user"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		mock.ExpectExec(`^ALTER USER "test-user" SET NETWORK_POLICY = "test-network-policy"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+
+		err := resources.CreateNetworkPolicyAttachment(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestNetworkPolicyAttachmentDelete(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"network_policy_name": "test-network-policy",
+		"set_for_account":     true,
+		"users":               []interface{}{"test-user"},
+	}
+	d := schema.TestResourceDataRaw(t, resources.NetworkPolicyAttachment().Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^ALTER ACCOUNT UNSET NETWORK_POLICY$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		mock.ExpectExec(`^DESCRIBE USER "test-user"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		mock.ExpectExec(`^ALTER USER "test-user" UNSET NETWORK_POLICY$`).WillReturnResult(sqlmock.NewResult(1, 1))
+
+		err := resources.DeleteNetworkPolicyAttachment(d, db)
+		r.NoError(err)
+	})
+}
diff --git a/pkg/resources/network_policy_test.go b/pkg/resources/network_policy_test.go
new file mode 100644
index 00000000..21a4592b
--- /dev/null
+++ b/pkg/resources/network_policy_test.go
@@ -0,0 +1,91 @@
+package resources_test
+
+import (
+	"database/sql"
+	"testing"
+	"time"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+
+	sqlmock "github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/require"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider"
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources"
+	. "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
+)
+
+func TestNetworkPolicy(t *testing.T) {
+	r := require.New(t)
+	err := resources.NetworkPolicy().InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestNetworkPolicyCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":            "test-network-policy",
+		"allowed_ip_list": []interface{}{"192.168.1.0/24"},
+		"blocked_ip_list": []interface{}{"155.548.2.98"},
+		"comment":         "great comment",
+	}
+	d := schema.TestResourceDataRaw(t, resources.NetworkPolicy().Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^CREATE NETWORK POLICY "test-network-policy" ALLOWED_IP_LIST=\('192\.168\.1\.0/24'\) BLOCKED_IP_LIST=\('155\.548\.2\.98'\) COMMENT="great comment"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		expectReadNetworkPolicy(mock)
+		err := resources.CreateNetworkPolicy(d, db)
+		r.NoError(err)
+	})
+}
+
+func expectReadNetworkPolicy(mock sqlmock.Sqlmock) {
+	showRows := sqlmock.NewRows([]string{
+		"created_on", "name", "comment", "entries_in_allowed_ip_list", "entries_in_blocked_ip_list",
+	}).AddRow(
+		time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "test-network-policy", "this is a comment", 2, 1,
+	)
+	mock.ExpectQuery(`^SHOW NETWORK POLICIES$`).WillReturnRows(showRows)
+
+	descRows := sqlmock.NewRows([]string{
+		"name", "value",
+	}).AddRow(
+		"ALLOWED_IP_LIST", "192.168.0.100,192.168.0.200/18",
+	).AddRow(
+		"BLOCKED_IP_LIST", "192.168.0.101",
+	)
+	mock.ExpectQuery(`^DESC NETWORK POLICY "test-network-policy"$`).WillReturnRows(descRows)
+}
+
+func TestNetworkPolicyDelete(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":            "test-network-policy",
+		"allowed_ip_list": []interface{}{"192.168.1.0/24"},
+		"blocked_ip_list": []interface{}{"155.548.2.98"},
+		"comment":         "great comment",
+	}
+	d := schema.TestResourceDataRaw(t, resources.NetworkPolicy().Schema, in)
+	d.SetId("test-network-policy")
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^DROP NETWORK POLICY "test-network-policy"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		err := resources.DeleteNetworkPolicy(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestIpListToString(t *testing.T) {
+	r := require.New(t)
+
+	in := []string{"192.168.0.100/24", "29.254.123.20"}
+	out := snowflake.IpListToString(in)
+
+	r.Equal("('192.168.0.100/24', '29.254.123.20')", out)
+}
diff --git a/pkg/snowflake/network_policy_test.go b/pkg/snowflake/network_policy_test.go
new file mode 100644
index 00000000..8a4e372e
--- /dev/null
+++ b/pkg/snowflake/network_policy_test.go
@@ -0,0 +1,123 @@
+package snowflake_test
+
+import (
+	"testing"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNetworkPolicyCreate(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	s.WithComment("This is a test comment")
+
+	allowedIps := []string{"192.168.0.100/24", "192.168.0.200/18"}
+	s.WithAllowedIpList(allowedIps)
+
+	blockedIps := []string{"29.254.123.20"}
+	s.WithBlockedIpList(blockedIps)
+
+	q := s.Create()
+	r.Equal(`CREATE NETWORK POLICY "test_network_policy" ALLOWED_IP_LIST=('192.168.0.100/24', '192.168.0.200/18') BLOCKED_IP_LIST=('29.254.123.20') COMMENT="This is a test comment"`, q)
+}
+
+func TestNetworkPolicyCreateNoOptionals(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	allowedIps := []string{"192.168.0.100/24", "192.168.0.200/18"}
+	s.WithAllowedIpList(allowedIps)
+
+	q := s.Create()
+	r.Equal(`CREATE NETWORK POLICY "test_network_policy" ALLOWED_IP_LIST=('192.168.0.100/24', '192.168.0.200/18')`, q)
+}
+
+func TestNetworkPolicyDescribe(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.Describe()
+	r.Equal(`DESC NETWORK POLICY "test_network_policy"`, q)
+}
+
+func TestNetworkPolicyDrop(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.Drop()
+	r.Equal(`DROP NETWORK POLICY "test_network_policy"`, q)
+}
+
+func TestNetworkPolicyChangeComment(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.ChangeComment("test comment!")
+	r.Equal(`ALTER NETWORK POLICY "test_network_policy" SET COMMENT = 'test comment!'`, q)
+}
+
+func TestNetworkPolicyRemoveComment(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.RemoveComment()
+	r.Equal(`ALTER NETWORK POLICY "test_network_policy" UNSET COMMENT`, q)
+}
+
+func TestNetworkPolicyChangeIpList(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	newAllowedIps := []string{"192.168.0.100/24", "29.254.123.20"}
+	q := s.ChangeIpList("ALLOWED", newAllowedIps)
+	r.Equal(`ALTER NETWORK POLICY "test_network_policy" SET ALLOWED_IP_LIST = ('192.168.0.100/24', '29.254.123.20')`, q)
+
+	var newBlockedIps []string
+	q = s.ChangeIpList("BLOCKED", newBlockedIps)
+	r.Equal(`ALTER NETWORK POLICY "test_network_policy" SET BLOCKED_IP_LIST = ()`, q)
+}
+
+func TestNetworkPolicySetOnAccount(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.SetOnAccount()
+	r.Equal(`ALTER ACCOUNT SET NETWORK_POLICY = "test_network_policy"`, q)
+}
+
+func TestNetworkPolicyUnsetOnAccount(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.UnsetOnAccount()
+	r.Equal(`ALTER ACCOUNT UNSET NETWORK_POLICY`, q)
+}
+
+func TestNetworkPolicySetOnUser(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.SetOnUser("testuser")
+	r.Equal(`ALTER USER "testuser" SET NETWORK_POLICY = "test_network_policy"`, q)
+}
+
+func TestNetworkPolicyUnsetOnUser(t *testing.T) {
+	r := require.New(t)
+	s := snowflake.NetworkPolicy("test_network_policy")
+	r.NotNil(s)
+
+	q := s.UnsetOnUser("testuser")
+	r.Equal(`ALTER USER "testuser" UNSET NETWORK_POLICY`, q)
+}