Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #556 (2021-05-31 09:46:36)

Add SCIM Integration Resource
Added SCIM Integration Resource (#549)

## Test Plan
* [x] acceptance tests
* [x] unit tests…

## References
See https://docs.snowflake.com/en/user-guide/scim.html

* 

ChrisIsidora: @alldoami This is related to the feature request #549 
ChrisIsidora: @alldoami can you trigger the test after the latest changes?
alldoami: /ok-to-test sha=691e58e
github-actions[bot]: Integration tests failure for [691e58e](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/897456632)
ChrisIsidora: @alldoami  Removed Enabled property which cannot be set for SECURITY INTEGRATION with type=SCIM
github-actions[bot]: Integration tests failure for [a17e7f5](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/897641233)
ChrisIsidora: @alldoami Added the dependent resources to the acceptance tests. Can you check if it's ok now?
github-actions[bot]: Integration tests failure for [808f6d2](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/897898215)
alldoami: This would be best if you could test locally and request my review when ready. 
ChrisIsidora: @alldoami You are right, sorry for the inconvenience. I tested the acceptance test locally, made some fixes and it seems to work now.

![image](https://user-images.githubusercontent.com/7924584/120439911-58a0d700-c383-11eb-9434-1a515b031f35.png)

alldoami: /ok-to-test sha=2cb5b18
github-actions[bot]: Integration tests failure for [2cb5b18](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/900689506)
ChrisIsidora: @alldoami Not sure why the acc test are failing. It does work on my snowflake account. See below, I made some changes to the resource id maybe that might help. For the unit test I saw that for the Generic alter builder there was no sorting which means that my alter unit test might fail sometimes because of the random order of the statement. That should be fixed now.

![image](https://user-images.githubusercontent.com/7924584/120557060-a65a2580-c3fd-11eb-9534-9e23e82ee34f.png)

alldoami: /ok-to-test sha=5bd10cb
github-actions[bot]: Integration tests failure for [5bd10cb](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/903749396)
ChrisIsidora: @alldoami  Seems that okta provider already exists on the acc snowflake account. I removed Okta tests
alldoami: /ok-to-test sha=f863df6
github-actions[bot]: Integration tests success for [f863df6](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/903869989)
ChrisIsidora: @alldoami can this be merged so that I can work / update #557? Thanks in advance for your patience
ChrisIsidora: @alldoami  and @jacobrheath Can you please review the changes requested. Thanks!
alldoami: /ok-to-test sha=15a53ae
github-actions[bot]: Integration tests success for [15a53ae](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/906905536)

Patch:
diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md
new file mode 100644
index 00000000..3ba17dd6
--- /dev/null
+++ b/docs/resources/scim_integration.md
@@ -0,0 +1,49 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "snowflake_scim_integration Resource - terraform-provider-snowflake"
+subcategory: ""
+description: |-
+  
+---
+
+# snowflake_scim_integration (Resource)
+
+
+
+## Example Usage
+
+```terraform
+resource "snowflake_scim_integration" "aad" {
+  name             = "AAD_PROVISIONING"
+  network_policy   = "AAD_NETWORK_POLICY"
+  provisioner_role = "AAD_PROVISIONER"
+  scim_client      = "AZURE"
+  enabled          = true
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- **name** (String) Specifies the name of the SCIM integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.
+- **provisioner_role** (String) Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM.
+- **scim_client** (String) Specifies the client type for the scim integration
+
+### Optional
+
+- **id** (String) The ID of this resource.
+- **network_policy** (String) Specifies an existing network policy active for your account. The network policy restricts the list of user IP addresses when exchanging an authorization code for an access or refresh token and when using a refresh token to obtain a new access token. If this parameter is not set, the network policy for the account (if any) is used instead.
+
+### Read-Only
+
+- **created_on** (String) Date and time when the SCIM integration was created.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import snowflake_scim_integration.example name
+```
diff --git a/examples/resources/snowflake_scim_integration/import.sh b/examples/resources/snowflake_scim_integration/import.sh
new file mode 100644
index 00000000..86e162e4
--- /dev/null
+++ b/examples/resources/snowflake_scim_integration/import.sh
@@ -0,0 +1,1 @@
+terraform import snowflake_scim_integration.example name
diff --git a/examples/resources/snowflake_scim_integration/resource.tf b/examples/resources/snowflake_scim_integration/resource.tf
new file mode 100644
index 00000000..dfb96116
--- /dev/null
+++ b/examples/resources/snowflake_scim_integration/resource.tf
@@ -0,0 +1,7 @@
+resource "snowflake_scim_integration" "aad" {
+  name             = "AAD_PROVISIONING"
+  network_policy   = "AAD_NETWORK_POLICY"
+  provisioner_role = "AAD_PROVISIONER"
+  scim_client      = "AZURE"
+  enabled          = true
+}
\ No newline at end of file
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 5c9a080a..2c5748fe 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -167,6 +167,7 @@ func getResources() map[string]*schema.Resource {
 		"snowflake_role":                      resources.Role(),
 		"snowflake_role_grants":               resources.RoleGrants(),
 		"snowflake_schema":                    resources.Schema(),
+		"snowflake_scim_integration":          resources.SCIMIntegration(),
 		"snowflake_share":                     resources.Share(),
 		"snowflake_stage":                     resources.Stage(),
 		"snowflake_storage_integration":       resources.StorageIntegration(),
diff --git a/pkg/resources/scim_integration.go b/pkg/resources/scim_integration.go
new file mode 100644
index 00000000..458c25bf
--- /dev/null
+++ b/pkg/resources/scim_integration.go
@@ -0,0 +1,212 @@
+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 scimIntegrationSchema = map[string]*schema.Schema{
+	"name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		ForceNew:    true,
+		Description: "Specifies the name of the SCIM integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.",
+	},
+	"scim_client": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specifies the client type for the scim integration",
+		ValidateFunc: validation.StringInSlice([]string{
+			"OKTA", "AZURE", "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)
+		},
+	},
+	"provisioner_role": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM.",
+		ValidateFunc: validation.StringInSlice([]string{
+			"OKTA_PROVISIONER", "AAD_PROVISIONER", "GENERIC_SCIM_PROVISIONER",
+		}, 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)
+		},
+	},
+	"network_policy": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "Specifies an existing network policy active for your account. The network policy restricts the list of user IP addresses when exchanging an authorization code for an access or refresh token and when using a refresh token to obtain a new access token. If this parameter is not set, the network policy for the account (if any) is used instead.",
+	},
+	"created_on": {
+		Type:        schema.TypeString,
+		Computed:    true,
+		Description: "Date and time when the SCIM integration was created.",
+	},
+}
+
+// SCIMIntegration returns a pointer to the resource representing a network policy
+func SCIMIntegration() *schema.Resource {
+	return &schema.Resource{
+		Create: CreateSCIMIntegration,
+		Read:   ReadSCIMIntegration,
+		Update: UpdateSCIMIntegration,
+		Delete: DeleteSCIMIntegration,
+
+		Schema: scimIntegrationSchema,
+		Importer: &schema.ResourceImporter{
+			StateContext: schema.ImportStatePassthroughContext,
+		},
+	}
+}
+
+// CreateSCIMIntegration implements schema.CreateFunc
+func CreateSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := d.Get("name").(string)
+
+	stmt := snowflake.ScimIntegration(name).Create()
+
+	// Set required fields
+	stmt.SetRaw(`TYPE=SCIM`)
+	stmt.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
+	stmt.SetString(`RUN_AS_ROLE`, d.Get("provisioner_role").(string))
+
+	// Set optional fields
+	if _, ok := d.GetOk("network_policy"); ok {
+		stmt.SetString(`NETWORK_POLICY`, d.Get("network_policy").(string))
+	}
+
+	err := snowflake.Exec(db, stmt.Statement())
+	if err != nil {
+		return errors.Wrap(err, "error creating security integration")
+	}
+
+	d.SetId(name)
+
+	return ReadSCIMIntegration(d, meta)
+}
+
+// ReadSCIMIntegration implements schema.ReadFunc
+func ReadSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	id := d.Id()
+
+	stmt := snowflake.ScimIntegration(id).Show()
+	row := snowflake.QueryRow(db, stmt)
+
+	// Some properties can come from the SHOW INTEGRATION call
+
+	s, err := snowflake.ScanScimIntegration(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("scim_client", strings.TrimPrefix(s.IntegrationType.String, "SCIM - ")); err != nil {
+		return err
+	}
+
+	if err := d.Set("name", s.Name.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.ScimIntegration(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 "NETWORK_POLICY":
+			if err = d.Set("network_policy", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set network policy for security integration")
+			}
+		case "RUN_AS_ROLE":
+			if err = d.Set("provisioner_role", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set provisioner role for security integration")
+			}
+		default:
+			log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k)
+		}
+	}
+
+	return err
+}
+
+// UpdateSCIMIntegration implements schema.UpdateFunc
+func UpdateSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	id := d.Id()
+
+	stmt := snowflake.ScimIntegration(id).Alter()
+
+	var runSetStatement bool
+
+	if d.HasChange("scim_client") {
+		runSetStatement = true
+		stmt.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
+	}
+
+	if d.HasChange("provisioner_role") {
+		runSetStatement = true
+		stmt.SetString(`RUN_AS_ROLE`, d.Get("provisioner_role").(string))
+	}
+
+	// We need to UNSET this if we remove all api blocked prefixes.
+	if d.HasChange("network_policy") {
+		v := d.Get("network_policy").(string)
+		if len(v) == 0 {
+			err := snowflake.Exec(db, fmt.Sprintf(`ALTER SECURITY INTEGRATION %v UNSET NETWORK_POLICY`, id))
+			if err != nil {
+				return errors.Wrap(err, "error unsetting network_policy")
+			}
+		} else {
+			runSetStatement = true
+			stmt.SetString(`NETWORK_POLICY`, d.Get("network_policy").(string))
+		}
+	}
+
+	if runSetStatement {
+		if err := snowflake.Exec(db, stmt.Statement()); err != nil {
+			return errors.Wrap(err, "error updating security integration")
+		}
+	}
+
+	return ReadSCIMIntegration(d, meta)
+}
+
+// DeleteSCIMIntegration implements schema.DeleteFunc
+func DeleteSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
+	return DeleteResource("", snowflake.ScimIntegration)(d, meta)
+}
diff --git a/pkg/snowflake/generic.go b/pkg/snowflake/generic.go
index 513bf1d3..2957fb5c 100644
--- a/pkg/snowflake/generic.go
+++ b/pkg/snowflake/generic.go
@@ -3,7 +3,6 @@ package snowflake
 import (
 	"bytes"
 	"fmt"
-	"sort"
 	"strings"
 	"text/template"
 )
@@ -11,15 +10,16 @@ import (
 type EntityType string
 
 const (
-	ApiIntegrationType     EntityType = "API INTEGRATION"
-	DatabaseType           EntityType = "DATABASE"
-	ManagedAccountType     EntityType = "MANAGED ACCOUNT"
-	ResourceMonitorType    EntityType = "RESOURCE MONITOR"
-	RoleType               EntityType = "ROLE"
-	ShareType              EntityType = "SHARE"
-	StorageIntegrationType EntityType = "STORAGE INTEGRATION"
-	UserType               EntityType = "USER"
-	WarehouseType          EntityType = "WAREHOUSE"
+	ApiIntegrationType      EntityType = "API INTEGRATION"
+	DatabaseType            EntityType = "DATABASE"
+	ManagedAccountType      EntityType = "MANAGED ACCOUNT"
+	ResourceMonitorType     EntityType = "RESOURCE MONITOR"
+	RoleType                EntityType = "ROLE"
+	SecurityIntegrationType EntityType = "SECURITY INTEGRATION"
+	ShareType               EntityType = "SHARE"
+	StorageIntegrationType  EntityType = "STORAGE INTEGRATION"
+	UserType                EntityType = "USER"
+	WarehouseType           EntityType = "WAREHOUSE"
 )
 
 type Builder struct {
@@ -108,24 +108,24 @@ func (ab *AlterPropertiesBuilder) Statement() string {
 
 	sb.WriteString(ab.rawStatement)
 
-	for k, v := range ab.stringProperties {
-		sb.WriteString(fmt.Sprintf(" %s='%s'", strings.ToUpper(k), EscapeString(v)))
+	for _, k := range sortStrings(ab.stringProperties) {
+		sb.WriteString(fmt.Sprintf(" %s='%s'", strings.ToUpper(k), EscapeString(ab.stringProperties[k])))
 	}
 
-	for k, v := range ab.stringListProperties {
-		sb.WriteString(fmt.Sprintf(" %s=%s", strings.ToUpper(k), formatStringList(v)))
+	for _, k := range sortStringList(ab.stringListProperties) {
+		sb.WriteString(fmt.Sprintf(" %s=%s", strings.ToUpper(k), formatStringList(ab.stringListProperties[k])))
 	}
 
-	for k, v := range ab.boolProperties {
-		sb.WriteString(fmt.Sprintf(" %s=%t", strings.ToUpper(k), v))
+	for _, k := range sortStringsBool(ab.boolProperties) {
+		sb.WriteString(fmt.Sprintf(" %s=%t", strings.ToUpper(k), ab.boolProperties[k]))
 	}
 
-	for k, v := range ab.intProperties {
-		sb.WriteString(fmt.Sprintf(" %s=%d", strings.ToUpper(k), v))
+	for _, k := range sortStringsInt(ab.intProperties) {
+		sb.WriteString(fmt.Sprintf(" %s=%d", strings.ToUpper(k), ab.intProperties[k]))
 	}
 
-	for k, v := range ab.floatProperties {
-		sb.WriteString(fmt.Sprintf(" %s=%.2f", strings.ToUpper(k), v))
+	for _, k := range sortStringsFloat(ab.floatProperties) {
+		sb.WriteString(fmt.Sprintf(" %s=%.2f", strings.ToUpper(k), ab.floatProperties[k]))
 	}
 
 	return sb.String()
@@ -186,52 +186,23 @@ func (b *CreateBuilder) Statement() string {
 
 	sb.WriteString(b.rawStatement)
 
-	sortedStringProperties := make([]string, 0)
-	for k := range b.stringProperties {
-		sortedStringProperties = append(sortedStringProperties, k)
-	}
-	sort.Strings(sortedStringProperties)
-
-	for _, k := range sortedStringProperties {
+	for _, k := range sortStrings(b.stringProperties) {
 		sb.WriteString(fmt.Sprintf(" %s='%s'", strings.ToUpper(k), EscapeString(b.stringProperties[k])))
 	}
 
-	sortedStringListProperties := make([]string, 0)
-	for k := range b.stringListProperties {
-		sortedStringListProperties = append(sortedStringListProperties, k)
-	}
-
-	for _, k := range sortedStringListProperties {
+	for _, k := range sortStringList(b.stringListProperties) {
 		sb.WriteString(fmt.Sprintf(" %s=%s", strings.ToUpper(k), formatStringList(b.stringListProperties[k])))
 	}
 
-	sortedBoolProperties := make([]string, 0)
-	for k := range b.boolProperties {
-		sortedBoolProperties = append(sortedBoolProperties, k)
-	}
-	sort.Strings(sortedBoolProperties)
-
-	for _, k := range sortedBoolProperties {
+	for _, k := range sortStringsBool(b.boolProperties) {
 		sb.WriteString(fmt.Sprintf(" %s=%t", strings.ToUpper(k), b.boolProperties[k]))
 	}
 
-	sortedIntProperties := make([]string, 0)
-	for k := range b.intProperties {
-		sortedIntProperties = append(sortedIntProperties, k)
-	}
-	sort.Strings(sortedIntProperties)
-
-	for _, k := range sortedIntProperties {
+	for _, k := range sortStringsInt(b.intProperties) {
 		sb.WriteString(fmt.Sprintf(" %s=%d", strings.ToUpper(k), b.intProperties[k]))
 	}
 
-	sortedFloatProperties := make([]string, 0)
-	for k := range b.floatProperties {
-		sortedFloatProperties = append(sortedFloatProperties, k)
-	}
-	sort.Strings(sortedFloatProperties)
-
-	for _, k := range sortedFloatProperties {
+	for _, k := range sortStringsFloat(b.floatProperties) {
 		sb.WriteString(fmt.Sprintf(" %s=%.2f", strings.ToUpper(k), b.floatProperties[k]))
 	}
 
diff --git a/pkg/snowflake/scim_integration.go b/pkg/snowflake/scim_integration.go
new file mode 100644
index 00000000..5039ccb1
--- /dev/null
+++ b/pkg/snowflake/scim_integration.go
@@ -0,0 +1,37 @@
+package snowflake
+
+import (
+	"database/sql"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/pkg/errors"
+)
+
+// ScimIntegration 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 ScimIntegration(name string) *Builder {
+	return &Builder{
+		entityType: SecurityIntegrationType,
+		name:       name,
+	}
+}
+
+type scimIntegration struct {
+	Name            sql.NullString `db:"name"`
+	Category        sql.NullString `db:"category"`
+	IntegrationType sql.NullString `db:"type"`
+	CreatedOn       sql.NullString `db:"created_on"`
+}
+
+func ScanScimIntegration(row *sqlx.Row) (*scimIntegration, error) {
+	r := &scimIntegration{}
+	return r, errors.Wrap(row.StructScan(r), "error scanning struct")
+}
diff --git a/pkg/snowflake/sorting.go b/pkg/snowflake/sorting.go
new file mode 100644
index 00000000..cd4babfe
--- /dev/null
+++ b/pkg/snowflake/sorting.go
@@ -0,0 +1,50 @@
+package snowflake
+
+import (
+	"sort"
+)
+
+func sortStrings(strs map[string]string) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}
+
+func sortStringList(strs map[string][]string) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}
+
+func sortStringsInt(strs map[string]int) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}
+
+func sortStringsFloat(strs map[string]float64) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}
+
+func sortStringsBool(strs map[string]bool) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}


Test patch:
diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go
index ff87873c..5bbe4795 100644
--- a/pkg/resources/helpers_test.go
+++ b/pkg/resources/helpers_test.go
@@ -184,6 +184,14 @@ func apiIntegration(t *testing.T, id string, params map[string]interface{}) *sch
 	return d
 }
 
+func scimIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
+	r := require.New(t)
+	d := schema.TestResourceDataRaw(t, resources.SCIMIntegration().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/scim_integration_acceptance_test.go b/pkg/resources/scim_integration_acceptance_test.go
new file mode 100644
index 00000000..90445cbf
--- /dev/null
+++ b/pkg/resources/scim_integration_acceptance_test.go
@@ -0,0 +1,83 @@
+package resources_test
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAcc_ScimIntegration(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_SCIM_INTEGRATION_TESTS"); ok {
+		t.Skip("Skipping TestAccScimIntegration")
+	}
+
+	scimIntName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+	scimProvisionerRole := "AAD_PROVISIONER"
+	scimNetworkPolicy := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+
+	resource.ParallelTest(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: scimIntegrationConfig_azure(scimIntName, scimProvisionerRole, scimNetworkPolicy),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_scim_integration.test", "name", scimIntName),
+					resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "AZURE"),
+					resource.TestCheckResourceAttr("snowflake_scim_integration.test", "provisioner_role", scimProvisionerRole),
+					resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", scimNetworkPolicy),
+					resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"),
+				),
+			},
+			{
+				ResourceName:      "snowflake_scim_integration.test",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
+func scimIntegrationConfig_azure(name string, role string, policy string) string {
+	return fmt.Sprintf(`
+	resource "snowflake_role" "azure" {
+		name = "%s"
+		comment = "test comment"
+	}
+
+	resource "snowflake_account_grant" "azurecua" {
+		roles     = [snowflake_role.azure.name]
+		privilege = "CREATE USER"
+	}
+
+	resource "snowflake_account_grant" "azurecra" {
+		roles     = [snowflake_role.azure.name]
+		privilege = "CREATE ROLE"
+	}
+
+	resource "snowflake_role_grants" "azure" {
+		role_name = snowflake_role.azure.name
+		roles = ["ACCOUNTADMIN"]
+	}
+
+	resource "snowflake_network_policy" "azure" {
+		name            = "%s"
+		allowed_ip_list = ["192.168.0.100/24", "29.254.123.20"]
+	}
+
+	resource "snowflake_scim_integration" "test" {
+		name = "%s"
+		scim_client = "AZURE"
+		provisioner_role = snowflake_role.azure.name
+		network_policy = snowflake_network_policy.azure.name
+		depends_on = [
+			snowflake_account_grant.azurecua,
+			snowflake_account_grant.azurecra,
+			snowflake_role_grants.azure
+		]
+	}
+	`, role, policy, name)
+}
diff --git a/pkg/resources/scim_integration_test.go b/pkg/resources/scim_integration_test.go
new file mode 100644
index 00000000..2c373c95
--- /dev/null
+++ b/pkg/resources/scim_integration_test.go
@@ -0,0 +1,81 @@
+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 TestSCIMIntegration(t *testing.T) {
+	r := require.New(t)
+	err := resources.SCIMIntegration().InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestSCIMIntegrationCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":             "test_scim_integration",
+		"scim_client":      "AZURE",
+		"provisioner_role": "AAD_PROVISIONER",
+		"network_policy":   "AAD_NETWORK_POLICY",
+	}
+	d := schema.TestResourceDataRaw(t, resources.SCIMIntegration().Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(
+			`^CREATE SECURITY INTEGRATION "test_scim_integration" TYPE=SCIM NETWORK_POLICY='AAD_NETWORK_POLICY' RUN_AS_ROLE='AAD_PROVISIONER' SCIM_CLIENT='AZURE'$`,
+		).WillReturnResult(sqlmock.NewResult(1, 1))
+		expectReadSCIMIntegration(mock)
+
+		err := resources.CreateSCIMIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestSCIMIntegrationRead(t *testing.T) {
+	r := require.New(t)
+
+	d := scimIntegration(t, "test_scim_integration", map[string]interface{}{"name": "test_scim_integration"})
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		expectReadSCIMIntegration(mock)
+
+		err := resources.ReadSCIMIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestSCIMIntegrationDelete(t *testing.T) {
+	r := require.New(t)
+
+	d := scimIntegration(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.DeleteSCIMIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func expectReadSCIMIntegration(mock sqlmock.Sqlmock) {
+	showRows := sqlmock.NewRows([]string{
+		"name", "type", "category", "created_on"},
+	).AddRow("test_scim_integration", "SCIM - AZURE", "SECURITY", "now")
+	mock.ExpectQuery(`^SHOW SECURITY INTEGRATIONS LIKE 'test_scim_integration'$`).WillReturnRows(showRows)
+
+	descRows := sqlmock.NewRows([]string{
+		"property", "property_type", "property_value", "property_default",
+	}).AddRow("NETWORK_POLICY", "String", "AAD_NETWORK_POLICY", nil).
+		AddRow("RUN_AS_ROLE", "String", "AAD_PROVISIONER", nil)
+
+	mock.ExpectQuery(`DESCRIBE SECURITY INTEGRATION "test_scim_integration"$`).WillReturnRows(descRows)
+}
diff --git a/pkg/snowflake/scim_integration_test.go b/pkg/snowflake/scim_integration_test.go
new file mode 100644
index 00000000..91e17f19
--- /dev/null
+++ b/pkg/snowflake/scim_integration_test.go
@@ -0,0 +1,38 @@
+package snowflake_test
+
+import (
+	"testing"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/stretchr/testify/require"
+)
+
+func TestScimIntegration(t *testing.T) {
+	r := require.New(t)
+	builder := snowflake.ScimIntegration("aad_provisioning")
+	r.NotNil(builder)
+
+	q := builder.Show()
+	r.Equal("SHOW SECURITY INTEGRATIONS LIKE 'aad_provisioning'", q)
+
+	q = builder.Describe()
+	r.Equal("DESCRIBE SECURITY INTEGRATION \"aad_provisioning\"", q)
+
+	c := builder.Create()
+	c.SetRaw(`TYPE=scim`)
+	c.SetString(`scim_client`, "azure")
+	c.SetString(`run_as_role`, "AAD_PROVISIONER")
+	q = c.Statement()
+	r.Equal(`CREATE SECURITY INTEGRATION "aad_provisioning" TYPE=scim RUN_AS_ROLE='AAD_PROVISIONER' SCIM_CLIENT='azure'`, q)
+
+	d := builder.Alter()
+	d.SetRaw(`TYPE=scim`)
+	d.SetString(`scim_client`, "azure")
+	d.SetString(`run_as_role`, "AAD_PROVISIONER")
+	d.SetString(`network_policy`, "aad_policy")
+	q = d.Statement()
+	r.Equal(`ALTER SECURITY INTEGRATION "aad_provisioning" SET TYPE=scim NETWORK_POLICY='aad_policy' RUN_AS_ROLE='AAD_PROVISIONER' SCIM_CLIENT='azure'`, q)
+
+	e := builder.Drop()
+	r.Equal(`DROP SECURITY INTEGRATION "aad_provisioning"`, e)
+}
diff --git a/pkg/snowflake/sorting_test.go b/pkg/snowflake/sorting_test.go
new file mode 100644
index 00000000..92f40fdb
--- /dev/null
+++ b/pkg/snowflake/sorting_test.go
@@ -0,0 +1,19 @@
+package snowflake
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestSortStrings(t *testing.T) {
+	r := require.New(t)
+
+	ss := []string{"a", "b", "c"}
+
+	r.Equal(ss, sortStrings(map[string]string{"c": "", "b": "", "a": ""}))
+	r.Equal(ss, sortStringList(map[string][]string{"c": {}, "b": {}, "a": {}}))
+	r.Equal(ss, sortStringsInt(map[string]int{"c": 0, "b": 1, "a": 2}))
+	r.Equal(ss, sortStringsFloat(map[string]float64{"c": 0, "b": 1, "a": 2}))
+	r.Equal(ss, sortStringsBool(map[string]bool{"c": true, "b": false, "a": true}))
+}