Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #763 (2021-11-19 11:59:58)

feat: OAuth security integration for partner applications
<!-- Feel free to delete comments as you fill this in -->

<!-- summary of changes -->

Implementing OAuth security integration, largely based on the existing SCIM security integration that's quite similar. 

This one only covers the **partner applications** (i.e. Tableau Desktop, Tableau Server and Looker), however extending it to support custom OAuth clients would just involve adding [required](https://docs.snowflake.com/en/sql-reference/sql/create-security-integration.html#snowflake-oauth-custom-client-parameters) and [optional](https://docs.snowflake.com/en/sql-reference/sql/create-security-integration.html#snowflake-oauth-custom-client-parameters-optional) parameters.

## Test Plan
<!-- detail ways in which this PR has been tested or needs to be tested -->
* [x] acceptance tests

## References
<!-- issues documentation links, etc  -->

* [Documentation](https://docs.snowflake.com/en/user-guide/oauth-partner.html)
* [CREATE](https://docs.snowflake.com/en/sql-reference/sql/create-security-integration.html)
* [ALTER](https://docs.snowflake.com/en/sql-reference/sql/alter-security-integration.html)
* [SHOW](https://docs.snowflake.com/en/sql-reference/sql/show-integrations.html)
* [DESCRIBE](https://docs.snowflake.com/en/sql-reference/sql/desc-integration.html)

edulop91: /ok-to-test sha=d10af87
github-actions[bot]: Integration tests failure for [d10af87](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1495613910)
edulop91: > Integration tests failure for [d10af87](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1495613910)

Looks like there might be a panic here: https://github.com/chanzuckerberg/terraform-provider-snowflake/runs/4301715646?check_suite_focus=true#step:9:1050 (not sure if this log is hidden behind GitHub access controls)
gouline: I can't get acceptance tests to run locally, so hopefully this fixes it. Found a similar instance in network policies and copied it:

```go
expandStringList(d.Get("blocked_roles_list").(*schema.Set).List())
```
alldoami: /ok-to-test sha=3eb42aa
github-actions[bot]: Integration tests failure for [3eb42aa](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1501556807)
gouline: CI step `integration-trusted` failed but I have no access to it, so cannot see what is causing it.
alldoami: ```
=== CONT  TestAcc_OAuthIntegration
panic: interface conversion: interface {} is nil, not bool

goroutine 60401 [running]:
github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources.CreateOAuthIntegration(0x0, {0x14b1940, 0xc002a0c680})
	/home/runner/work/terraform-provider-snowflake/terraform-provider-snowflake/pkg/resources/oauth_integration.go:99 +0x7c5
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).create(0x1330a20, {0x17f2748, 0xc000f6ce80}, 0x2, {0x14b1940, 0xc002a0c680})
	/home/runner/go/pkg/mod/github.com/hashicorp/terraform-plugin-sdk/v2@v2.8.0/helper/schema/resource.go:318 +0x178
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).Apply(0xc00073cb60, {0x17f2748, 0xc000f6ce80}, 0xc00053c270, 0xc003091580, {0x14b1940, 0xc002a0c680})
	/home/runner/go/pkg/mod/github.com/hashicorp/terraform-plugin-sdk/v2@v2.8.0/helper/schema/resource.go:456 +0x871
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*GRPCProviderServer).ApplyResourceChange(0xc003b0c1b0, {0x17f2748, 0xc000f6ce80}, 0xc003e37900)
	/home/runner/go/pkg/mod/github.com/hashicorp/terraform-plugin-sdk/v2@v2.8.0/helper/schema/grpc_provider.go:977 +0xd8a
github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server.(*server).ApplyResourceChange(0xc0026ff4e0, {0x17f27f0, 0xc001833b60}, 0x4423e0)
	/home/runner/go/pkg/mod/github.com/hashicorp/terraform-plugin-go@v0.4.0/tfprotov5/tf5server/server.go:332 +0x6c
github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5._Provider_ApplyResourceChange_Handler({0x14722e0, 0xc0026ff4e0}, {0x17f27f0, 0xc001833b60}, 0xc002634e40, 0x0)
	/home/runner/go/pkg/mod/github.com/hashicorp/terraform-plugin-go@v0.4.0/tfprotov5/internal/tfplugin5/tfplugin5_grpc.pb.go:380 +0x170
google.golang.org/grpc.(*Server).processUnaryRPC(0xc00217e540, {0x180adc8, 0xc0025a7b00}, 0xc00299ca20, 0xc0036934a0, 0x2152ee0, 0x0)
	/home/runner/go/pkg/mod/google.golang.org/grpc@v1.41.0/server.go:1279 +0xccf
google.golang.org/grpc.(*Server).handleStream(0xc00217e540, {0x180adc8, 0xc0025a7b00}, 0xc00299ca20, 0x0)
	/home/runner/go/pkg/mod/google.golang.org/grpc@v1.41.0/server.go:1608 +0xa2a
google.golang.org/grpc.(*Server).serveStreams.func1.2()
	/home/runner/go/pkg/mod/google.golang.org/grpc@v1.41.0/server.go:923 +0x98
created by google.golang.org/grpc.(*Server).serveStreams.func1
	/home/runner/go/pkg/mod/google.golang.org/grpc@v1.41.0/server.go:921 +0x294
FAIL	github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources	356.529s
```
When you click on the link in this comment, what does it say? https://github.com/chanzuckerberg/terraform-provider-snowflake/pull/763#issuecomment-978467694
gouline: > When you click on the link in this comment, what does it say? [#763 (comment)](https://github.com/chanzuckerberg/terraform-provider-snowflake/pull/763#issuecomment-978467694)

Sorry, this works. I was previously clicking on 'integration-trusted' in the footer (force of habit).
gouline: Found the problem! Silly copy-paste typo 🤦 
alldoami: /ok-to-test sha=ba75402
github-actions[bot]: Integration tests failure for [ba75402](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1519259769)
gouline: Hmm, are these all meant to be treated as strings and parsed to booleans/integers? https://github.com/gouline/terraform-provider-snowflake/blob/161f112/pkg/resources/oauth_integration.go#L186-L201

I assumed from the `ENABLED` parameter fed as a boolean in all unit tests, they should be treated as booleans/integers, but `ENABLED` is always ignored in the `switch` statement so it might just be copy-pasted mistake that nobody picked up?

```go
descRows := sqlmock.NewRows([]string{
		"property", "property_type", "property_value", "property_default",
	}).AddRow("ENABLED", "Boolean", true, false)
```

Latest commit treats them as strings, so hopefully that works 🤷 
alldoami: /ok-to-test sha=c165355
github-actions[bot]: Integration tests failure for [c165355](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1522359673)
gouline: Previous error looks fixed now, latest commit should fix the new error.
alldoami: /ok-to-test sha=e09bf36
github-actions[bot]: Integration tests failure for [e09bf36](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1523511954)
gouline: Should be fixed again.
alldoami: /ok-to-test sha=c2634c8
github-actions[bot]: Integration tests failure for [c2634c8](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1523708408)
gouline: This one was interesting: `blocked_roles_list` should only include roles not already implicitly enforced (ACCOUNTADMIN, ORGADMIN, SECURITYADMIN) on create, so I had to filter them out from the read. Should work now.
alldoami: /ok-to-test sha=e40332d
github-actions[bot]: Integration tests success for [e40332d](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1524314530)

Patch:
diff --git a/docs/resources/oauth_integration.md b/docs/resources/oauth_integration.md
new file mode 100644
index 00000000..772abbca
--- /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 generated by tfplugindocs -->
+## 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 00000000..cbbb03d1
--- /dev/null
+++ b/examples/resources/snowflake_oauth_integration/import.sh
@@ -0,0 +1,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 00000000..d28900d9
--- /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 95c589cd..fd148bb1 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/oauth_integration.go b/pkg/resources/oauth_integration.go
new file mode 100644
index 00000000..599700ff
--- /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/snowflake/oauth_integration.go b/pkg/snowflake/oauth_integration.go
new file mode 100644
index 00000000..d3e2323c
--- /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")
+}


Test patch:
diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go
index c07b3170..1e4b0d3d 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_acceptance_test.go b/pkg/resources/oauth_integration_acceptance_test.go
new file mode 100644
index 00000000..dfbc3ca7
--- /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 00000000..1c213b17
--- /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 603df0f2..dbecc3f4 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_test.go b/pkg/snowflake/oauth_integration_test.go
new file mode 100644
index 00000000..707fcd06
--- /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)
+}