diff --git a/docs/resources/external_oauth_integration.md b/docs/resources/external_oauth_integration.md new file mode 100644 index 0000000000..046c95cfed --- /dev/null +++ b/docs/resources/external_oauth_integration.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_external_oauth_integration Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_external_oauth_integration (Resource) + + + +## Example Usage + +```terraform +resource "snowflake_external_oauth_integration" "azure" { + name = "AZURE_POWERBI" + type = "AZURE" + enabled = true + issuer = "https://sts.windows.net/00000000-0000-0000-0000-000000000000" + snowflake_user_mapping_attribute = "LOGIN_NAME" + jws_keys_urls = ["https://login.windows.net/common/discovery/keys"] + audience_urls = ["https://analysis.windows.net/powerbi/connector/Snowflake"] + token_user_mapping_claims = ["upn"] +} +``` + + +## Schema + +### Required + +- **enabled** (Boolean) Specifies whether to initiate operation of the integration or suspend it. +- **issuer** (String) Specifies the URL to define the OAuth 2.0 authorization server. +- **name** (String) Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- **snowflake_user_mapping_attribute** (String) Indicates which Snowflake user record attribute should be used to map the access token to a Snowflake user record. +- **token_user_mapping_claims** (Set of String) Specifies the access token claim or claims that can be used to map the access token to a Snowflake user record. +- **type** (String) Specifies the OAuth 2.0 authorization server to be Okta, Microsoft Azure AD, Ping Identity PingFederate, or a Custom OAuth 2.0 authorization server. + +### Optional + +- **allowed_roles** (Set of String) Specifies the list of roles that the client can set as the primary role. +- **any_role_mode** (String) Specifies whether the OAuth client or user can use a role that is not defined in the OAuth access token. +- **audience_urls** (Set of String) Specifies additional values that can be used for the access token's audience validation on top of using the Customer's Snowflake Account URL +- **blocked_roles** (Set of String) Specifies the list of roles that a client cannot set as the primary role. Do not include ACCOUNTADMIN, ORGADMIN or SECURITYADMIN as they are already implicitly enforced and will cause in-place updates. +- **comment** (String) Specifies a comment for the OAuth integration. +- **id** (String) The ID of this resource. +- **jws_keys_urls** (Set of String) Specifies the endpoint or a list of endpoints from which to download public keys or certificates to validate an External OAuth access token. The maximum number of URLs that can be specified in the list is 3. +- **rsa_public_key** (String) Specifies a Base64-encoded RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers. +- **rsa_public_key_2** (String) Specifies a second RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers. Used for key rotation. +- **scope_delimiter** (String) Specifies the scope delimiter in the authorization token. + +### Read-Only + +- **created_on** (String) Date and time when the External OAUTH integration was created. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_external_oauth_integration.example name +``` diff --git a/examples/resources/snowflake_external_oauth_integration/import.sh b/examples/resources/snowflake_external_oauth_integration/import.sh new file mode 100644 index 0000000000..feb48d066e --- /dev/null +++ b/examples/resources/snowflake_external_oauth_integration/import.sh @@ -0,0 +1 @@ +terraform import snowflake_external_oauth_integration.example name diff --git a/examples/resources/snowflake_external_oauth_integration/resource.tf b/examples/resources/snowflake_external_oauth_integration/resource.tf new file mode 100644 index 0000000000..d96c9234f9 --- /dev/null +++ b/examples/resources/snowflake_external_oauth_integration/resource.tf @@ -0,0 +1,10 @@ +resource "snowflake_external_oauth_integration" "azure" { + name = "AZURE_POWERBI" + type = "AZURE" + enabled = true + issuer = "https://sts.windows.net/00000000-0000-0000-0000-000000000000" + snowflake_user_mapping_attribute = "LOGIN_NAME" + jws_keys_urls = ["https://login.windows.net/common/discovery/keys"] + audience_urls = ["https://analysis.windows.net/powerbi/connector/Snowflake"] + token_user_mapping_claims = ["upn"] +} \ No newline at end of file diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 7308724486..03fb510000 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -167,40 +167,41 @@ func GetGrantResources() resources.TerraformGrantResources { func getResources() map[string]*schema.Resource { // NOTE(): do not add grant resources here others := map[string]*schema.Resource{ - "snowflake_api_integration": resources.APIIntegration(), - "snowflake_database": resources.Database(), - "snowflake_external_function": resources.ExternalFunction(), - "snowflake_file_format": resources.FileFormat(), - "snowflake_function": resources.Function(), - "snowflake_managed_account": resources.ManagedAccount(), - "snowflake_masking_policy": resources.MaskingPolicy(), - "snowflake_materialized_view": resources.MaterializedView(), - "snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(), - "snowflake_network_policy": resources.NetworkPolicy(), - "snowflake_oauth_integration": resources.OAuthIntegration(), - "snowflake_pipe": resources.Pipe(), - "snowflake_procedure": resources.Procedure(), - "snowflake_resource_monitor": resources.ResourceMonitor(), - "snowflake_role": resources.Role(), - "snowflake_role_grants": resources.RoleGrants(), - "snowflake_row_access_policy": resources.RowAccessPolicy(), - "snowflake_saml_integration": resources.SAMLIntegration(), - "snowflake_schema": resources.Schema(), - "snowflake_scim_integration": resources.SCIMIntegration(), - "snowflake_sequence": resources.Sequence(), - "snowflake_share": resources.Share(), - "snowflake_stage": resources.Stage(), - "snowflake_storage_integration": resources.StorageIntegration(), - "snowflake_notification_integration": resources.NotificationIntegration(), - "snowflake_stream": resources.Stream(), - "snowflake_table": resources.Table(), - "snowflake_external_table": resources.ExternalTable(), - "snowflake_tag": resources.Tag(), - "snowflake_task": resources.Task(), - "snowflake_user": resources.User(), - "snowflake_user_public_keys": resources.UserPublicKeys(), - "snowflake_view": resources.View(), - "snowflake_warehouse": resources.Warehouse(), + "snowflake_api_integration": resources.APIIntegration(), + "snowflake_database": resources.Database(), + "snowflake_external_function": resources.ExternalFunction(), + "snowflake_file_format": resources.FileFormat(), + "snowflake_function": resources.Function(), + "snowflake_managed_account": resources.ManagedAccount(), + "snowflake_masking_policy": resources.MaskingPolicy(), + "snowflake_materialized_view": resources.MaterializedView(), + "snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(), + "snowflake_network_policy": resources.NetworkPolicy(), + "snowflake_oauth_integration": resources.OAuthIntegration(), + "snowflake_external_oauth_integration": resources.ExternalOauthIntegration(), + "snowflake_pipe": resources.Pipe(), + "snowflake_procedure": resources.Procedure(), + "snowflake_resource_monitor": resources.ResourceMonitor(), + "snowflake_role": resources.Role(), + "snowflake_role_grants": resources.RoleGrants(), + "snowflake_row_access_policy": resources.RowAccessPolicy(), + "snowflake_saml_integration": resources.SAMLIntegration(), + "snowflake_schema": resources.Schema(), + "snowflake_scim_integration": resources.SCIMIntegration(), + "snowflake_sequence": resources.Sequence(), + "snowflake_share": resources.Share(), + "snowflake_stage": resources.Stage(), + "snowflake_storage_integration": resources.StorageIntegration(), + "snowflake_notification_integration": resources.NotificationIntegration(), + "snowflake_stream": resources.Stream(), + "snowflake_table": resources.Table(), + "snowflake_external_table": resources.ExternalTable(), + "snowflake_tag": resources.Tag(), + "snowflake_task": resources.Task(), + "snowflake_user": resources.User(), + "snowflake_user_public_keys": resources.UserPublicKeys(), + "snowflake_view": resources.View(), + "snowflake_warehouse": resources.Warehouse(), } return mergeSchemas( diff --git a/pkg/resources/external_oauth_integration.go b/pkg/resources/external_oauth_integration.go new file mode 100644 index 0000000000..b77a9d457c --- /dev/null +++ b/pkg/resources/external_oauth_integration.go @@ -0,0 +1,417 @@ +package resources + +import ( + "database/sql" + "fmt" + "log" + "strings" + + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/pkg/errors" +) + +var oauthExternalIntegrationSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: "Specifies the OAuth 2.0 authorization server to be Okta, Microsoft Azure AD, Ping Identity PingFederate, or a Custom OAuth 2.0 authorization server.", + ValidateFunc: validation.StringInSlice([]string{ + "OKTA", "AZURE", "PING_FEDERATE", "CUSTOM", + }, true), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + normalize := func(s string) string { + return strings.ToUpper(strings.Replace(s, "-", "", -1)) + } + return normalize(old) == normalize(new) + }, + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "Specifies whether to initiate operation of the integration or suspend it.", + }, + "issuer": { + Type: schema.TypeString, + Required: true, + Description: "Specifies the URL to define the OAuth 2.0 authorization server.", + }, + "token_user_mapping_claims": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Specifies the access token claim or claims that can be used to map the access token to a Snowflake user record.", + }, + "snowflake_user_mapping_attribute": { + Type: schema.TypeString, + Required: true, + Description: "Indicates which Snowflake user record attribute should be used to map the access token to a Snowflake user record.", + ValidateFunc: validation.StringInSlice([]string{ + "LOGIN_NAME", "EMAIL_ADDRESS", + }, true), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + normalize := func(s string) string { + return strings.ToUpper(strings.Replace(s, "-", "", -1)) + } + return normalize(old) == normalize(new) + }, + }, + "jws_keys_urls": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + MaxItems: 3, + Optional: true, + Description: "Specifies the endpoint or a list of endpoints from which to download public keys or certificates to validate an External OAuth access token. The maximum number of URLs that can be specified in the list is 3.", + }, + "rsa_public_key": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a Base64-encoded RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers.", + }, + "rsa_public_key_2": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a second RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers. Used for key rotation.", + }, + "blocked_roles": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "Specifies the list of roles that a client cannot set as the primary role. Do not include ACCOUNTADMIN, ORGADMIN or SECURITYADMIN as they are already implicitly enforced and will cause in-place updates.", + }, + "allowed_roles": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "Specifies the list of roles that the client can set as the primary role.", + }, + "audience_urls": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "Specifies additional values that can be used for the access token's audience validation on top of using the Customer's Snowflake Account URL ", + }, + "any_role_mode": { + Type: schema.TypeString, + Optional: true, + Default: "DISABLE", + Description: "Specifies whether the OAuth client or user can use a role that is not defined in the OAuth access token.", + ValidateFunc: validation.StringInSlice([]string{ + "DISABLE ", "ENABLE ", "ENABLE_FOR_PRIVILEGE", + }, true), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + normalize := func(s string) string { + return strings.ToUpper(strings.Replace(s, "-", "", -1)) + } + return normalize(old) == normalize(new) + }, + }, + "scope_delimiter": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the scope delimiter in the authorization token.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the OAuth integration.", + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time when the External OAUTH integration was created.", + }, +} + +// ExternalOauthIntegration returns a pointer to the resource representing a network policy +func ExternalOauthIntegration() *schema.Resource { + return &schema.Resource{ + Create: CreateExternalOauthIntegration, + Read: ReadExternalOauthIntegration, + Update: UpdateExternalOauthIntegration, + Delete: DeleteExternalOauthIntegration, + + Schema: oauthExternalIntegrationSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +// CreateExternalOauthIntegration implements schema.CreateFunc +func CreateExternalOauthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + name := d.Get("name").(string) + + stmt := snowflake.ExternalOauthIntegration(name).Create() + + // Set required fields + stmt.SetRaw(`TYPE=EXTERNAL_OAUTH`) + stmt.SetBool(`ENABLED`, d.Get("enabled").(bool)) + stmt.SetString(`EXTERNAL_OAUTH_TYPE`, d.Get("type").(string)) + stmt.SetString(`EXTERNAL_OAUTH_ISSUER`, d.Get("issuer").(string)) + stmt.SetStringList(`EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM`, expandStringList(d.Get("token_user_mapping_claims").(*schema.Set).List())) + stmt.SetString(`EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE`, d.Get("snowflake_user_mapping_attribute").(string)) + + // Set optional fields + if _, ok := d.GetOk("jws_keys_urls"); ok { + stmt.SetStringList(`EXTERNAL_OAUTH_JWS_KEYS_URL`, expandStringList(d.Get("jws_keys_urls").(*schema.Set).List())) + } + if _, ok := d.GetOk("rsa_public_key"); ok { + stmt.SetString(`EXTERNAL_OAUTH_RSA_PUBLIC_KEY`, d.Get("rsa_public_key").(string)) + } + if _, ok := d.GetOk("rsa_public_key_2"); ok { + stmt.SetString(`EXTERNAL_OAUTH_RSA_PUBLIC_KEY_2`, d.Get("rsa_public_key_2").(string)) + } + if _, ok := d.GetOk("blocked_roles"); ok { + stmt.SetStringList(`EXTERNAL_OAUTH_BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles").(*schema.Set).List())) + } + if _, ok := d.GetOk("allowed_roles"); ok { + stmt.SetStringList(`EXTERNAL_OAUTH_ALLOWED_ROLES_LIST`, expandStringList(d.Get("allowed_roles").(*schema.Set).List())) + } + if _, ok := d.GetOk("audience_urls"); ok { + stmt.SetStringList(`EXTERNAL_OAUTH_AUDIENCE_LIST`, expandStringList(d.Get("audience_urls").(*schema.Set).List())) + } + if _, ok := d.GetOk("any_role_mode"); ok { + stmt.SetString(`EXTERNAL_OAUTH_ANY_ROLE_MODE`, d.Get("any_role_mode").(string)) + } + if _, ok := d.GetOk("scope_delimiter"); ok { + stmt.SetString(`EXTERNAL_OAUTH_SCOPE_DELIMITER`, d.Get("scope_delimiter").(string)) + } + if _, ok := d.GetOk("comment"); ok { + stmt.SetString(`COMMENT`, d.Get("comment").(string)) + } + + err := snowflake.Exec(db, stmt.Statement()) + if err != nil { + return errors.Wrap(err, "error creating security integration"+stmt.Statement()) + } + + d.SetId(name) + + return ReadExternalOauthIntegration(d, meta) +} + +// ReadExternalOauthIntegration implements schema.ReadFunc +func ReadExternalOauthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + id := d.Id() + + stmt := snowflake.ExternalOauthIntegration(id).Show() + row := snowflake.QueryRow(db, stmt) + + // Some properties can come from the SHOW INTEGRATION call + + s, err := snowflake.ScanExternalOauthIntegration(row) + if err != nil { + return errors.Wrap(err, "could not show security integration") + } + + // Note: category must be Security or something is broken + if c := s.Category.String; c != "SECURITY" { + return fmt.Errorf("expected %v to be an Security integration, got %v", id, c) + } + + if err := d.Set("type", strings.TrimPrefix(s.IntegrationType.String, "EXTERNAL_OAUTH - ")); err != nil { + return err + } + + if err := d.Set("name", s.Name.String); err != nil { + return err + } + + if err := d.Set("enabled", s.Enabled.Bool); err != nil { + return err + } + + if err := d.Set("comment", s.Comment.String); err != nil { + return err + } + + if err := d.Set("created_on", s.CreatedOn.String); err != nil { + return err + } + + // Some properties come from the DESCRIBE INTEGRATION call + // We need to grab them in a loop + var k, pType string + var v, unused interface{} + stmt = snowflake.ExternalOauthIntegration(id).Describe() + rows, err := db.Query(stmt) + if err != nil { + return errors.Wrap(err, "could not describe security integration") + } + defer rows.Close() + for rows.Next() { + if err := rows.Scan(&k, &pType, &v, &unused); err != nil { + return errors.Wrap(err, "unable to parse security integration rows") + } + switch k { + case "ENABLED": + // We set this using the SHOW INTEGRATION call so let's ignore it here + case "COMMENT": + // We set this using the SHOW INTEGRATION call so let's ignore it here + case "EXTERNAL_OAUTH_ISSUER": + if err = d.Set("issuer", v.(string)); err != nil { + return errors.Wrap(err, "unable to set issuer for security integration") + } + case "EXTERNAL_OAUTH_JWS_KEYS_URL": + list := []string{} + list = append(list, strings.Split(v.(string), ",")...) + if err = d.Set("jws_keys_urls", list); err != nil { + return errors.Wrap(err, "unable to set jws keys urls for security integration") + } + case "EXTERNAL_OAUTH_ANY_ROLE_MODE": + if err = d.Set("any_role_mode", v.(string)); err != nil { + return errors.Wrap(err, "unable to set any role mode for security integration") + } + case "EXTERNAL_OAUTH_RSA_PUBLIC_KEY": + if err = d.Set("rsa_public_key", v.(string)); err != nil { + return errors.Wrap(err, "unable to set rsa public key for security integration") + } + case "EXTERNAL_OAUTH_RSA_PUBLIC_KEY_2": + if err = d.Set("rsa_public_key_2", v.(string)); err != nil { + return errors.Wrap(err, "unable to set rsa public key 2 for security integration") + } + case "EXTERNAL_OAUTH_BLOCKED_ROLES_LIST": + blockedRolesAll := strings.Split(v.(string), ",") + // Only roles other than ACCOUNTADMIN, ORGADMIN and SECURITYADMIN can be specified custom, + // those three are enforced with no option to remove them + blockedRolesCustom := []string{} + for _, role := range blockedRolesAll { + if role != "ACCOUNTADMIN" && role != "ORGADMIN" && role != "SECURITYADMIN" && role != "" { + blockedRolesCustom = append(blockedRolesCustom, role) + } + } + + if err = d.Set("blocked_roles", blockedRolesCustom); err != nil { + return errors.Wrap(err, "unable to set blocked roles for security integration") + } + case "EXTERNAL_OAUTH_ALLOWED_ROLES_LIST": + list := []string{} + for _, item := range strings.Split(v.(string), ",") { + if item != "" { + list = append(list, item) + } + } + if err = d.Set("allowed_roles", list); err != nil { + return errors.Wrap(err, "unable to set allowed roles for security integration") + } + case "EXTERNAL_OAUTH_AUDIENCE_LIST": + list := []string{} + for _, item := range strings.Split(v.(string), ",") { + if item != "" { + list = append(list, item) + } + } + if err = d.Set("audience_urls", list); err != nil { + return errors.Wrap(err, "unable to set audience urls for security integration") + } + case "EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM": + list := []string{} + for _, item := range strings.Split(strings.Replace(strings.Replace(v.(string), "[", "", 1), "]", "", 1), ",") { + if item != "" { + list = append(list, strings.Replace(item, "'", "", 2)) + } + } + if err = d.Set("token_user_mapping_claims", list); err != nil { + return errors.Wrap(err, "unable to set token user mapping claims for security integration") + } + case "EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE": + if err = d.Set("snowflake_user_mapping_attribute", v.(string)); err != nil { + return errors.Wrap(err, "unable to set snowflake mapping attribute for security integration") + } + default: + log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k) + } + } + + return err +} + +// UpdateExternalOauthIntegration implements schema.UpdateFunc +func UpdateExternalOauthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + id := d.Id() + + stmt := snowflake.ExternalOauthIntegration(id).Alter() + + var runSetStatement bool + + if d.HasChange("enabled") { + runSetStatement = true + stmt.SetBool(`ENABLED`, d.Get("enabled").(bool)) + } + if d.HasChange("type") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_TYPE`, d.Get("type").(string)) + } + if d.HasChange("issuer") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_ISSUER`, d.Get("issuer").(string)) + } + if d.HasChange("token_user_mapping_claims") { + runSetStatement = true + stmt.SetStringList(`EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM`, expandStringList(d.Get("token_user_mapping_claims").(*schema.Set).List())) + } + if d.HasChange("snowflake_user_mapping_attribute") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE`, d.Get("snowflake_user_mapping_attribute").(string)) + } + if d.HasChange("jws_keys_urls") { + runSetStatement = true + stmt.SetStringList(`EXTERNAL_OAUTH_JWS_KEYS_URL`, expandStringList(d.Get("jws_keys_urls").(*schema.Set).List())) + } + if d.HasChange("rsa_public_key") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_RSA_PUBLIC_KEY`, d.Get("rsa_public_key").(string)) + } + if d.HasChange("rsa_public_key_2") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_RSA_PUBLIC_KEY_2`, d.Get("rsa_public_key_2").(string)) + } + if d.HasChange("blocked_roles") { + runSetStatement = true + stmt.SetStringList(`EXTERNAL_OAUTH_BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles").(*schema.Set).List())) + } + if d.HasChange("allowed_roles") { + runSetStatement = true + stmt.SetStringList(`EXTERNAL_OAUTH_ALLOWED_ROLES_LIST`, expandStringList(d.Get("allowed_roles").(*schema.Set).List())) + } + if d.HasChange("audience_urls") { + runSetStatement = true + stmt.SetStringList(`EXTERNAL_OAUTH_AUDIENCE_LIST`, expandStringList(d.Get("audience_urls").(*schema.Set).List())) + } + if d.HasChange("any_role_mode") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_ANY_ROLE_MODE`, d.Get("any_role_mode").(string)) + } + if d.HasChange("scope_delimiter") { + runSetStatement = true + stmt.SetString(`EXTERNAL_OAUTH_SCOPE_DELIMITER`, d.Get("scope_delimiter").(string)) + } + if d.HasChange("comment") { + runSetStatement = true + stmt.SetString(`COMMENT`, d.Get("comment").(string)) + } + + if runSetStatement { + if err := snowflake.Exec(db, stmt.Statement()); err != nil { + return errors.Wrap(err, "error updating security integration") + } + } + + return ReadExternalOauthIntegration(d, meta) +} + +// DeleteExternalOauthIntegration implements schema.DeleteFunc +func DeleteExternalOauthIntegration(d *schema.ResourceData, meta interface{}) error { + return DeleteResource("", snowflake.ExternalOauthIntegration)(d, meta) +} diff --git a/pkg/resources/external_oauth_integration_acceptance_test.go b/pkg/resources/external_oauth_integration_acceptance_test.go new file mode 100644 index 0000000000..6f28d383f7 --- /dev/null +++ b/pkg/resources/external_oauth_integration_acceptance_test.go @@ -0,0 +1,53 @@ +package resources_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAcc_ExternalOauthIntegration(t *testing.T) { + oauthIntName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + integrationType := "AZURE" + + resource.ParallelTest(t, resource.TestCase{ + Providers: providers(), + Steps: []resource.TestStep{ + { + Config: externalOauthIntegrationConfig(oauthIntName, integrationType), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "name", oauthIntName), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "type", integrationType), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "enabled", "true"), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "issuer", "https://sts.windows.net/00000000-0000-0000-0000-000000000000"), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "snowflake_user_mapping_attribute", "LOGIN_NAME"), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "token_user_mapping_claims.#", "1"), + resource.TestCheckResourceAttr("snowflake_external_oauth_integration.test", "token_user_mapping_claims.0", "upn"), + ), + }, + { + ResourceName: "snowflake_external_oauth_integration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func externalOauthIntegrationConfig(name string, integrationType string) string { + return fmt.Sprintf(` + resource "snowflake_external_oauth_integration" "test" { + name = "%s" + type = "%s" + enabled = true + issuer = "https://sts.windows.net/00000000-0000-0000-0000-000000000000" + snowflake_user_mapping_attribute = "LOGIN_NAME" + jws_keys_urls = ["https://login.windows.net/common/discovery/keys"] + audience_urls = ["https://analysis.windows.net/powerbi/connector/Snowflake"] + token_user_mapping_claims = ["upn"] + } + `, name, integrationType) +} diff --git a/pkg/resources/external_oauth_integration_test.go b/pkg/resources/external_oauth_integration_test.go new file mode 100644 index 0000000000..92feef94e8 --- /dev/null +++ b/pkg/resources/external_oauth_integration_test.go @@ -0,0 +1,91 @@ +package resources_test + +import ( + "database/sql" + "testing" + + sqlmock "github.com/DATA-DOG/go-sqlmock" + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider" + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources" + . "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/require" +) + +func TestExternalOauthIntegration(t *testing.T) { + r := require.New(t) + err := resources.ExternalOauthIntegration().InternalValidate(provider.Provider().Schema, true) + r.NoError(err) +} + +func TestExternalOauthIntegrationCreate(t *testing.T) { + r := require.New(t) + + in := map[string]interface{}{ + "name": "test_external_oauth_integration", + "type": "AZURE", + "enabled": true, + "issuer": "https://sts.windows.net/00000000-0000-0000-0000-000000000000", + "snowflake_user_mapping_attribute": "LOGIN_NAME", + "token_user_mapping_claims": []interface{}{"upn"}, + } + d := schema.TestResourceDataRaw(t, resources.ExternalOauthIntegration().Schema, in) + r.NotNil(d) + + WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { + mock.ExpectExec( + `^CREATE SECURITY INTEGRATION "test_external_oauth_integration" TYPE=EXTERNAL_OAUTH EXTERNAL_OAUTH_ANY_ROLE_MODE='DISABLE' EXTERNAL_OAUTH_ISSUER='https://sts.windows.net/00000000-0000-0000-0000-000000000000' EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE='LOGIN_NAME' EXTERNAL_OAUTH_TYPE='AZURE' EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM=\('upn'\) ENABLED=true$`, + ).WillReturnResult(sqlmock.NewResult(1, 1)) + expectReadExternalOauthIntegration(mock) + + err := resources.CreateExternalOauthIntegration(d, db) + r.NoError(err) + }) +} + +func TestExternalOauthIntegrationRead(t *testing.T) { + r := require.New(t) + + d := externalOauthIntegration(t, "test_external_oauth_integration", map[string]interface{}{"name": "test_external_oauth_integration"}) + + WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { + expectReadExternalOauthIntegration(mock) + + err := resources.ReadExternalOauthIntegration(d, db) + r.NoError(err) + }) +} + +func TestExternalOauthIntegrationDelete(t *testing.T) { + r := require.New(t) + + d := externalOauthIntegration(t, "drop_it", map[string]interface{}{"name": "drop_it"}) + + WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { + mock.ExpectExec(`DROP SECURITY INTEGRATION "drop_it"`).WillReturnResult(sqlmock.NewResult(1, 1)) + err := resources.DeleteExternalOauthIntegration(d, db) + r.NoError(err) + }) +} + +func expectReadExternalOauthIntegration(mock sqlmock.Sqlmock) { + showRows := sqlmock.NewRows([]string{ + "name", "type", "category", "enabled", "comment", "created_on"}, + ).AddRow("test_external_oauth_integration", "EXTERNAL_OAUTH - AZURE", "SECURITY", true, nil, "now") + mock.ExpectQuery(`^SHOW SECURITY INTEGRATIONS LIKE 'test_external_oauth_integration'$`).WillReturnRows(showRows) + + descRows := sqlmock.NewRows([]string{ + "property", "property_type", "property_value", "property_default", + }).AddRow("EXTERNAL_OAUTH_ISSUER", "String", "https://sts.windows.net/00000000-0000-0000-0000-000000000000", nil). + AddRow("EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM", "List", "['upn']", nil). + AddRow("EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE", "String", "upn", nil). + AddRow("EXTERNAL_OAUTH_RSA_PUBLIC_KEY", "String", "", nil). + AddRow("EXTERNAL_OAUTH_RSA_PUBLIC_KEY_2", "String", "", nil). + AddRow("EXTERNAL_OAUTH_BLOCKED_ROLES_LIST", "List", "ACCOUNTADMIN,SECURITYADMIN", nil). + AddRow("EXTERNAL_OAUTH_JWS_KEYS_URL", "List", "", nil). + AddRow("EXTERNAL_OAUTH_ALLOWED_ROLES_LIST", "List", "", nil). + AddRow("EXTERNAL_OAUTH_AUDIENCE_LIST", "List", "", nil). + AddRow("EXTERNAL_OAUTH_ANY_ROLE_MODE", "String", "", nil) + + mock.ExpectQuery(`DESCRIBE SECURITY INTEGRATION "test_external_oauth_integration"$`).WillReturnRows(descRows) +} diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go index 28bcb11595..8a0e2a33d9 100644 --- a/pkg/resources/helpers_test.go +++ b/pkg/resources/helpers_test.go @@ -224,6 +224,14 @@ func oauthIntegration(t *testing.T, id string, params map[string]interface{}) *s return d } +func externalOauthIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData { + r := require.New(t) + d := schema.TestResourceDataRaw(t, resources.ExternalOauthIntegration().Schema, params) + r.NotNil(d) + d.SetId(id) + return d +} + func externalFunction(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData { r := require.New(t) d := schema.TestResourceDataRaw(t, resources.ExternalFunction().Schema, params) diff --git a/pkg/snowflake/external_oauth_integration.go b/pkg/snowflake/external_oauth_integration.go new file mode 100644 index 0000000000..014603a136 --- /dev/null +++ b/pkg/snowflake/external_oauth_integration.go @@ -0,0 +1,39 @@ +package snowflake + +import ( + "database/sql" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +// ExternalOauthIntegration returns a pointer to a Builder that abstracts the DDL operations for an api integration. +// +// Supported DDL operations are: +// - CREATE SECURITY INTEGRATION +// - ALTER SECURITY INTEGRATION +// - DROP INTEGRATION +// - SHOW INTEGRATIONS +// - DESCRIBE INTEGRATION +// +// [Snowflake Reference](https://docs.snowflake.com/en/sql-reference/ddl-user-security.html#security-integrations) +func ExternalOauthIntegration(name string) *Builder { + return &Builder{ + entityType: SecurityIntegrationType, + name: name, + } +} + +type externalOauthIntegration struct { + Name sql.NullString `db:"name"` + Category sql.NullString `db:"category"` + IntegrationType sql.NullString `db:"type"` + Enabled sql.NullBool `db:"enabled"` + Comment sql.NullString `db:"comment"` + CreatedOn sql.NullString `db:"created_on"` +} + +func ScanExternalOauthIntegration(row *sqlx.Row) (*externalOauthIntegration, error) { + r := &externalOauthIntegration{} + return r, errors.Wrap(row.StructScan(r), "error scanning struct") +} diff --git a/pkg/snowflake/external_oauth_integration_test.go b/pkg/snowflake/external_oauth_integration_test.go new file mode 100644 index 0000000000..62a83f003d --- /dev/null +++ b/pkg/snowflake/external_oauth_integration_test.go @@ -0,0 +1,29 @@ +package snowflake_test + +import ( + "testing" + + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake" + "github.com/stretchr/testify/require" +) + +func TestExternalOauthIntegration(t *testing.T) { + r := require.New(t) + builder := snowflake.ExternalOauthIntegration("azure") + r.NotNil(builder) + + q := builder.Show() + r.Equal("SHOW SECURITY INTEGRATIONS LIKE 'azure'", q) + + q = builder.Describe() + r.Equal("DESCRIBE SECURITY INTEGRATION \"azure\"", q) + + c := builder.Create() + c.SetRaw(`TYPE=EXTERNAL_OAUTH`) + c.SetString(`EXTERNAL_OAUTH_TYPE`, "AZURE") + q = c.Statement() + r.Equal(`CREATE SECURITY INTEGRATION "azure" TYPE=EXTERNAL_OAUTH EXTERNAL_OAUTH_TYPE='AZURE'`, q) + + e := builder.Drop() + r.Equal(`DROP SECURITY INTEGRATION "azure"`, e) +}