diff --git a/docs/resources/oauth_integration.md b/docs/resources/oauth_integration.md new file mode 100644 index 0000000000..772abbca15 --- /dev/null +++ b/docs/resources/oauth_integration.md @@ -0,0 +1,54 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_oauth_integration Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_oauth_integration (Resource) + + + +## Example Usage + +```terraform +resource "snowflake_oauth_integration" "tableau_desktop" { + name = "TABLEAU_DESKTOP" + oauth_client = "TABLEAU_DESKTOP" + enabled = true + oauth_issue_refresh_tokens = true + oauth_refresh_token_validity = 3600 + blocked_roles_list = ["SYSADMIN"] +} +``` + + +## Schema + +### Required + +- **name** (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- **oauth_client** (String) Specifies the OAuth client type. + +### Optional + +- **blocked_roles_list** (Set of String) List of roles that a user cannot explicitly consent to using after authenticating. 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. +- **enabled** (Boolean) Specifies whether this OAuth integration is enabled or disabled. +- **id** (String) The ID of this resource. +- **oauth_issue_refresh_tokens** (Boolean) Specifies whether to allow the client to exchange a refresh token for an access token when the current access token has expired. +- **oauth_refresh_token_validity** (Number) Specifies how long refresh tokens should be valid (in seconds). OAUTH_ISSUE_REFRESH_TOKENS must be set to TRUE. +- **oauth_use_secondary_roles** (String) Specifies whether default secondary roles set in the user properties are activated by default in the session being opened. + +### Read-Only + +- **created_on** (String) Date and time when the OAuth integration was created. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_oauth_integration.example name +``` diff --git a/examples/resources/snowflake_oauth_integration/import.sh b/examples/resources/snowflake_oauth_integration/import.sh new file mode 100644 index 0000000000..cbbb03d1ea --- /dev/null +++ b/examples/resources/snowflake_oauth_integration/import.sh @@ -0,0 +1 @@ +terraform import snowflake_oauth_integration.example name diff --git a/examples/resources/snowflake_oauth_integration/resource.tf b/examples/resources/snowflake_oauth_integration/resource.tf new file mode 100644 index 0000000000..d28900d9ce --- /dev/null +++ b/examples/resources/snowflake_oauth_integration/resource.tf @@ -0,0 +1,8 @@ +resource "snowflake_oauth_integration" "tableau_desktop" { + name = "TABLEAU_DESKTOP" + oauth_client = "TABLEAU_DESKTOP" + enabled = true + oauth_issue_refresh_tokens = true + oauth_refresh_token_validity = 3600 + blocked_roles_list = ["SYSADMIN"] +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 95c589cd89..fd148bb14d 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -177,6 +177,7 @@ func getResources() map[string]*schema.Resource { "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(), diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go index c07b317041..1e4b0d3d73 100644 --- a/pkg/resources/helpers_test.go +++ b/pkg/resources/helpers_test.go @@ -208,6 +208,14 @@ func scimIntegration(t *testing.T, id string, params map[string]interface{}) *sc return d } +func oauthIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData { + r := require.New(t) + d := schema.TestResourceDataRaw(t, resources.OAuthIntegration().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/resources/oauth_integration.go b/pkg/resources/oauth_integration.go new file mode 100644 index 0000000000..599700ff2c --- /dev/null +++ b/pkg/resources/oauth_integration.go @@ -0,0 +1,302 @@ +package resources + +import ( + "database/sql" + "fmt" + "log" + "strconv" + "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 oauthIntegrationSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + }, + "oauth_client": { + Type: schema.TypeString, + Required: true, + Description: "Specifies the OAuth client type.", + ValidateFunc: validation.StringInSlice([]string{ + "TABLEAU_DESKTOP", "TABLEAU_SERVER", "LOOKER", + }, false), + }, + "oauth_issue_refresh_tokens": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether to allow the client to exchange a refresh token for an access token when the current access token has expired.", + }, + "oauth_refresh_token_validity": { + Type: schema.TypeInt, + Optional: true, + Description: "Specifies how long refresh tokens should be valid (in seconds). OAUTH_ISSUE_REFRESH_TOKENS must be set to TRUE.", + }, + "oauth_use_secondary_roles": { + Type: schema.TypeString, + Optional: true, + Default: "NONE", + Description: "Specifies whether default secondary roles set in the user properties are activated by default in the session being opened.", + ValidateFunc: validation.StringInSlice([]string{ + "IMPLICIT", "NONE", + }, false), + }, + "blocked_roles_list": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "List of roles that a user cannot explicitly consent to using after authenticating. Do not include ACCOUNTADMIN, ORGADMIN or SECURITYADMIN as they are already implicitly enforced and will cause in-place updates.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the OAuth integration.", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether this OAuth integration is enabled or disabled.", + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + Description: "Date and time when the OAuth integration was created.", + }, +} + +// OAuthIntegration returns a pointer to the resource representing an OAuth integration +func OAuthIntegration() *schema.Resource { + return &schema.Resource{ + Create: CreateOAuthIntegration, + Read: ReadOAuthIntegration, + Update: UpdateOAuthIntegration, + Delete: DeleteOAuthIntegration, + + Schema: oauthIntegrationSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +// CreateOAuthIntegration implements schema.CreateFunc +func CreateOAuthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + name := d.Get("name").(string) + + stmt := snowflake.OAuthIntegration(name).Create() + + // Set required fields + stmt.SetRaw(`TYPE=OAUTH`) + stmt.SetString(`OAUTH_CLIENT`, d.Get("oauth_client").(string)) + + // Set optional fields + if _, ok := d.GetOk("oauth_issue_refresh_tokens"); ok { + stmt.SetBool(`OAUTH_ISSUE_REFRESH_TOKENS`, d.Get("oauth_issue_refresh_tokens").(bool)) + } + if _, ok := d.GetOk("oauth_refresh_token_validity"); ok { + stmt.SetInt(`OAUTH_REFRESH_TOKEN_VALIDITY`, d.Get("oauth_refresh_token_validity").(int)) + } + if _, ok := d.GetOk("oauth_use_secondary_roles"); ok { + stmt.SetString(`OAUTH_USE_SECONDARY_ROLES`, d.Get("oauth_use_secondary_roles").(string)) + } + if _, ok := d.GetOk("blocked_roles_list"); ok { + stmt.SetStringList(`BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles_list").(*schema.Set).List())) + } + if _, ok := d.GetOk("enabled"); ok { + stmt.SetBool(`ENABLED`, d.Get("enabled").(bool)) + } + 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") + } + + d.SetId(name) + + return ReadOAuthIntegration(d, meta) +} + +// ReadOAuthIntegration implements schema.ReadFunc +func ReadOAuthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + id := d.Id() + + stmt := snowflake.OAuthIntegration(id).Show() + row := snowflake.QueryRow(db, stmt) + + // Some properties can come from the SHOW INTEGRATION call + + s, err := snowflake.ScanOAuthIntegration(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("oauth_client", strings.TrimPrefix(s.IntegrationType.String, "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.OAuthIntegration(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 "OAUTH_ISSUE_REFRESH_TOKENS": + b, err := strconv.ParseBool(v.(string)) + if err != nil { + return errors.Wrap(err, "returned OAuth issue refresh tokens that is not boolean") + } + if err = d.Set("oauth_issue_refresh_tokens", b); err != nil { + return errors.Wrap(err, "unable to set OAuth issue refresh tokens for security integration") + } + case "OAUTH_REFRESH_TOKEN_VALIDITY": + i, err := strconv.Atoi(v.(string)) + if err != nil { + return errors.Wrap(err, "returned OAuth refresh token validity that is not integer") + } + if err = d.Set("oauth_refresh_token_validity", i); err != nil { + return errors.Wrap(err, "unable to set OAuth refresh token validity for security integration") + } + case "OAUTH_USE_SECONDARY_ROLES": + if err = d.Set("oauth_use_secondary_roles", v.(string)); err != nil { + return errors.Wrap(err, "unable to set OAuth use secondary roles for security integration") + } + case "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" { + blockedRolesCustom = append(blockedRolesCustom, role) + } + } + + if err = d.Set("blocked_roles_list", blockedRolesCustom); err != nil { + return errors.Wrap(err, "unable to set blocked roles list for security integration") + } + case "OAUTH_CLIENT_TYPE": + // Only used for custom OAuth clients (not supported yet) + case "OAUTH_ENFORCE_PKCE": + // Only used for custom OAuth clients (not supported yet) + case "OAUTH_AUTHORIZATION_ENDPOINT": + // Only used for custom OAuth clients (not supported yet) + case "OAUTH_TOKEN_ENDPOINT": + // Only used for custom OAuth clients (not supported yet) + case "OAUTH_ALLOWED_AUTHORIZATION_ENDPOINTS": + // Only used for custom OAuth clients (not supported yet) + case "OAUTH_ALLOWED_TOKEN_ENDPOINTS": + // Only used for custom OAuth clients (not supported yet) + case "PRE_AUTHORIZED_ROLES_LIST": + // Only used for custom OAuth clients (not supported yet) + + default: + log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k) + } + } + + return err +} + +// UpdateOAuthIntegration implements schema.UpdateFunc +func UpdateOAuthIntegration(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + id := d.Id() + + stmt := snowflake.OAuthIntegration(id).Alter() + + var runSetStatement bool + + if d.HasChange("oauth_client") { + runSetStatement = true + stmt.SetString(`OAUTH_CLIENT`, d.Get("oauth_client").(string)) + } + + if d.HasChange("oauth_issue_refresh_tokens") { + runSetStatement = true + stmt.SetBool(`OAUTH_ISSUE_REFRESH_TOKENS`, d.Get("oauth_issue_refresh_tokens").(bool)) + } + + if d.HasChange("oauth_refresh_token_validity") { + runSetStatement = true + stmt.SetInt(`OAUTH_REFRESH_TOKEN_VALIDITY`, d.Get("oauth_refresh_token_validity").(int)) + } + + if d.HasChange("oauth_use_secondary_roles") { + runSetStatement = true + stmt.SetString(`OAUTH_USE_SECONDARY_ROLES`, d.Get("oauth_use_secondary_roles").(string)) + } + + if d.HasChange("blocked_roles_list") { + runSetStatement = true + stmt.SetStringList(`BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles_list").([]interface{}))) + } + + if d.HasChange("enabled") { + runSetStatement = true + stmt.SetBool(`ENABLED`, d.Get("enabled").(bool)) + } + + 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 ReadOAuthIntegration(d, meta) +} + +// DeleteOAuthIntegration implements schema.DeleteFunc +func DeleteOAuthIntegration(d *schema.ResourceData, meta interface{}) error { + return DeleteResource("", snowflake.OAuthIntegration)(d, meta) +} diff --git a/pkg/resources/oauth_integration_acceptance_test.go b/pkg/resources/oauth_integration_acceptance_test.go new file mode 100644 index 0000000000..dfbc3ca7f0 --- /dev/null +++ b/pkg/resources/oauth_integration_acceptance_test.go @@ -0,0 +1,50 @@ +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_OAuthIntegration(t *testing.T) { + oauthIntName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + integrationType := "TABLEAU_SERVER" + + resource.ParallelTest(t, resource.TestCase{ + Providers: providers(), + Steps: []resource.TestStep{ + { + Config: oauthIntegrationConfig(oauthIntName, integrationType), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "name", oauthIntName), + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "oauth_client", integrationType), + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "oauth_issue_refresh_tokens", "true"), + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "oauth_refresh_token_validity", "3600"), + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "blocked_roles_list.#", "1"), + resource.TestCheckResourceAttr("snowflake_oauth_integration.test", "blocked_roles_list.0", "SYSADMIN"), + ), + }, + { + ResourceName: "snowflake_oauth_integration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func oauthIntegrationConfig(name string, integrationType string) string { + return fmt.Sprintf(` + resource "snowflake_oauth_integration" "test" { + name = "%s" + oauth_client = "%s" + enabled = true + oauth_issue_refresh_tokens = true + oauth_refresh_token_validity = 3600 + blocked_roles_list = ["SYSADMIN"] + } + `, name, integrationType) +} diff --git a/pkg/resources/oauth_integration_test.go b/pkg/resources/oauth_integration_test.go new file mode 100644 index 0000000000..1c213b17dc --- /dev/null +++ b/pkg/resources/oauth_integration_test.go @@ -0,0 +1,80 @@ +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 TestOAuthIntegration(t *testing.T) { + r := require.New(t) + err := resources.OAuthIntegration().InternalValidate(provider.Provider().Schema, true) + r.NoError(err) +} + +func TestOAuthIntegrationCreate(t *testing.T) { + r := require.New(t) + + in := map[string]interface{}{ + "name": "test_oauth_integration", + "oauth_client": "TABLEAU_DESKTOP", + } + d := schema.TestResourceDataRaw(t, resources.OAuthIntegration().Schema, in) + r.NotNil(d) + + WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { + mock.ExpectExec( + `^CREATE SECURITY INTEGRATION "test_oauth_integration" TYPE=OAUTH OAUTH_CLIENT='TABLEAU_DESKTOP' OAUTH_USE_SECONDARY_ROLES='NONE'$`, + ).WillReturnResult(sqlmock.NewResult(1, 1)) + expectReadOAuthIntegration(mock) + + err := resources.CreateOAuthIntegration(d, db) + r.NoError(err) + }) +} + +func TestOAuthIntegrationRead(t *testing.T) { + r := require.New(t) + + d := oauthIntegration(t, "test_oauth_integration", map[string]interface{}{"name": "test_oauth_integration"}) + + WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { + expectReadOAuthIntegration(mock) + + err := resources.ReadOAuthIntegration(d, db) + r.NoError(err) + }) +} + +func TestOAuthIntegrationDelete(t *testing.T) { + r := require.New(t) + + d := oauthIntegration(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.DeleteOAuthIntegration(d, db) + r.NoError(err) + }) +} + +func expectReadOAuthIntegration(mock sqlmock.Sqlmock) { + showRows := sqlmock.NewRows([]string{ + "name", "type", "category", "enabled", "comment", "created_on"}, + ).AddRow("test_oauth_integration", "OAUTH - TABLEAU_DESKTOP", "SECURITY", true, nil, "now") + mock.ExpectQuery(`^SHOW SECURITY INTEGRATIONS LIKE 'test_oauth_integration'$`).WillReturnRows(showRows) + + descRows := sqlmock.NewRows([]string{ + "property", "property_type", "property_value", "property_default", + }).AddRow("OAUTH_ISSUE_REFRESH_TOKENS", "Boolean", "true", "true"). + AddRow("OAUTH_REFRESH_TOKEN_VALIDITY", "Integer", "86400", "7776000"). + AddRow("BLOCKED_ROLES_LIST", "List", "ACCOUNTADMIN,SECURITYADMIN", nil) + + mock.ExpectQuery(`DESCRIBE SECURITY INTEGRATION "test_oauth_integration"$`).WillReturnRows(descRows) +} diff --git a/pkg/resources/storage_integration_test.go b/pkg/resources/storage_integration_test.go index 603df0f293..dbecc3f4c3 100644 --- a/pkg/resources/storage_integration_test.go +++ b/pkg/resources/storage_integration_test.go @@ -66,7 +66,7 @@ func TestStorageIntegrationReadEmpty(t *testing.T) { err := resources.ReadStorageIntegration(d, db) r.Nil(err) - }) + }) } func TestStorageIntegrationUpdate(t *testing.T) { diff --git a/pkg/snowflake/oauth_integration.go b/pkg/snowflake/oauth_integration.go new file mode 100644 index 0000000000..d3e2323c45 --- /dev/null +++ b/pkg/snowflake/oauth_integration.go @@ -0,0 +1,39 @@ +package snowflake + +import ( + "database/sql" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +// OAuthIntegration 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 OAuthIntegration(name string) *Builder { + return &Builder{ + entityType: SecurityIntegrationType, + name: name, + } +} + +type oauthIntegration 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 ScanOAuthIntegration(row *sqlx.Row) (*oauthIntegration, error) { + r := &oauthIntegration{} + return r, errors.Wrap(row.StructScan(r), "error scanning struct") +} diff --git a/pkg/snowflake/oauth_integration_test.go b/pkg/snowflake/oauth_integration_test.go new file mode 100644 index 0000000000..707fcd061c --- /dev/null +++ b/pkg/snowflake/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 TestOAuthIntegration(t *testing.T) { + r := require.New(t) + builder := snowflake.OAuthIntegration("tableau_desktop") + r.NotNil(builder) + + q := builder.Show() + r.Equal("SHOW SECURITY INTEGRATIONS LIKE 'tableau_desktop'", q) + + q = builder.Describe() + r.Equal("DESCRIBE SECURITY INTEGRATION \"tableau_desktop\"", q) + + c := builder.Create() + c.SetRaw(`TYPE=oauth`) + c.SetString(`oauth_client`, "tableau_desktop") + q = c.Statement() + r.Equal(`CREATE SECURITY INTEGRATION "tableau_desktop" TYPE=oauth OAUTH_CLIENT='tableau_desktop'`, q) + + e := builder.Drop() + r.Equal(`DROP SECURITY INTEGRATION "tableau_desktop"`, e) +}