diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md
new file mode 100644
index 00000000000..14e7996ed73
--- /dev/null
+++ b/docs/resources/network_policy.md
@@ -0,0 +1,13 @@
+
+# snowflake_network_policy
+
+
+
+## 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
**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 00000000000..439273e74e3
--- /dev/null
+++ b/docs/resources/network_policy_attachment.md
@@ -0,0 +1,12 @@
+
+# snowflake_network_policy_attachment
+
+
+
+## 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
**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.
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 f8cd6f2fafb..ddb4569398c 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -68,31 +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_stream": resources.Stream(),
- "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 00000000000..e0af1d817ac
--- /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
**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_acceptance_test.go b/pkg/resources/network_policy_acceptance_test.go
new file mode 100644
index 00000000000..b3a44cbea40
--- /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.go b/pkg/resources/network_policy_attachment.go
new file mode 100644
index 00000000000..b50a7dd8440
--- /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
**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.
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/resources/network_policy_attachment_acceptance_test.go b/pkg/resources/network_policy_attachment_acceptance_test.go
new file mode 100644
index 00000000000..16a6348d921
--- /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 00000000000..e7f40a0f585
--- /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 00000000000..21a4592b237
--- /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.go b/pkg/snowflake/network_policy.go
new file mode 100644
index 00000000000..5c514ed167c
--- /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
+}
diff --git a/pkg/snowflake/network_policy_test.go b/pkg/snowflake/network_policy_test.go
new file mode 100644
index 00000000000..8a4e372edbf
--- /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)
+}