From 4dd6077e720135ee8516623fca2afce448fb592b Mon Sep 17 00:00:00 2001 From: Graham Place Date: Fri, 23 Oct 2020 12:34:13 -0700 Subject: [PATCH] [feature] Network Policy Resource (#271) Adds support for managing Snowflake Network Policy objects via the new `snowflake_network_policy` resource type. ## Test Plan * [x] acceptance tests * [x] tested built provider locally with trial Snowflake account ## References * Issue: https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/221 * https://docs.snowflake.com/en/sql-reference/sql/create-network-policy.html --- docs/resources/network_policy.md | 13 + docs/resources/network_policy_attachment.md | 12 + pkg/provider/provider.go | 50 ++-- pkg/resources/network_policy.go | 224 ++++++++++++++++ .../network_policy_acceptance_test.go | 74 +++++ pkg/resources/network_policy_attachment.go | 253 ++++++++++++++++++ ...twork_policy_attachment_acceptance_test.go | 61 +++++ .../network_policy_attachment_test.go | 63 +++++ pkg/resources/network_policy_test.go | 91 +++++++ pkg/snowflake/network_policy.go | 145 ++++++++++ pkg/snowflake/network_policy_test.go | 123 +++++++++ 11 files changed, 1085 insertions(+), 24 deletions(-) create mode 100644 docs/resources/network_policy.md create mode 100644 docs/resources/network_policy_attachment.md create mode 100644 pkg/resources/network_policy.go create mode 100644 pkg/resources/network_policy_acceptance_test.go create mode 100644 pkg/resources/network_policy_attachment.go create mode 100644 pkg/resources/network_policy_attachment_acceptance_test.go create mode 100644 pkg/resources/network_policy_attachment_test.go create mode 100644 pkg/resources/network_policy_test.go create mode 100644 pkg/snowflake/network_policy.go create mode 100644 pkg/snowflake/network_policy_test.go diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md new file mode 100644 index 0000000000..14e7996ed7 --- /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 0000000000..439273e74e --- /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 3f81ba2279..ddb4569398 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 0000000000..e0af1d817a --- /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 0000000000..b3a44cbea4 --- /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 0000000000..b50a7dd844 --- /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 0000000000..16a6348d92 --- /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 0000000000..e7f40a0f58 --- /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 0000000000..21a4592b23 --- /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 0000000000..5c514ed167 --- /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 0000000000..8a4e372edb --- /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) +}