Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #805 (2022-01-05 17:06:53)

feat: Allow creation of saml2 integrations (#616)
- Allows creation of security integrations with type=SAML2 ([example](https://docs.snowflake.com/en/sql-reference/sql/create-security-integration.html#id6))

## Test Plan
Created integration and acceptance tests

## References
This PR should address issue #616 
https://docs.snowflake.com/en/sql-reference/ddl-user-security.html#security-integrations
https://docs.snowflake.com/en/sql-reference/sql/create-security-integration.html#id6

alldoami: /ok-to-test sha=45875c5
github-actions[bot]: Integration tests failure for [45875c5](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1659742853)
intinfinite: > Integration tests failure for [45875c5](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1659742853)

Is this an issue on my end?
alldoami: @intinfinite yes, you can see the logs if you click on the link in the comment.
intinfinite: Thanks, I think these should fix it
alldoami: /ok-to-test sha=6ec69e7
github-actions[bot]: Integration tests failure for [6ec69e7](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1664216486)
intinfinite: Looks like it's failing because a SAML2 integration already exists on the account.

```
=== RUN   TestAcc_SamlIntegration
    saml_integration_acceptance_test.go:20: Step 1/1 error: Error running apply: exit status 1
        
        Error: error creating security integration: 390956 (22023): There can only be one enabled SAML2 integration
        
          on terraform_plugin_test.tf line 2, in resource "snowflake_saml_integration" "test_saml_int":
           2: 	resource "snowflake_saml_integration" "test_saml_int" {
        
        
--- FAIL: TestAcc_SamlIntegration (2.40s)
```

Do you know if there is an active SAML2 integration on the runner's Snowflake account the tests are run on or if the integration I created wasn't dropped since the first test failed?

You can check by seeing if any records are returned when running the below with ACCOUNTADMIN:
```sql
show security integrations;
```

If there is a SAML2 integration being used on the account, I can change the test to create a disabled security integration instead.

Thanks!
alldoami: /ok-to-test sha=7498921
alldoami: Hi @intinfinite! Thanks for the thorough comment. Yes we are using the SAML2 Integration, so the test to disable sounds good.
github-actions[bot]: Integration tests failure for [7498921](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1668862989)
intinfinite: Sorry for the multiple iterations.  Think this should be the last one *fingers crossed*.

It looks like there is a remnant `SECURITY INTEGRATION` from one of the previous failures that's preventing this from being run and would need to be manually dropped/destroyed before this test will pass.

```
=== RUN   TestAcc_SamlIntegration
    saml_integration_acceptance_test.go:20: Step 1/1 error: Error running apply: exit status 1
        
        Error: error creating security integration: 390957 (22023): Issuer URLs must be distinct across SAML2 integrations.
        
          on terraform_plugin_test.tf line 2, in resource "snowflake_saml_integration" "test_saml_int":
           2: 	resource "snowflake_saml_integration" "test_saml_int" {
        
        
--- FAIL: TestAcc_SamlIntegration (3.09s)
```

Can view if there are any test integrations like this:
```sql
SHOW SECURITY INTEGRATIONS LIKE '%test%';
```

And then drop them like this:
```
DROP SECURITY INTEGRATION "test_saml_integration";
```
intinfinite: You should be able to have multiple security integrations of the same type, just cannot have more than one enabled at a time (see screenshot below).

I updated the test to create a disabled integration, but I think there might be a test integration that's leftover from a previous run that failed before it could be removed.

![image](https://user-images.githubusercontent.com/14199416/148614363-569e33f8-34b4-4edd-8e87-78767d6f0935.png)
alldoami: I checked for the `test_saml_integration` but do not see it on my end...
alldoami: /ok-to-test sha=7498921
intinfinite: Thanks for checking.  I think I see what the issue is. 

I used to have the name hard-coded as "test-saml-integration", but changed it to be random to be more in line with the other tests (like the API integration or scim integration).

You can be sure that it's the dummy integration that was created before by describing it using:

```sql
desc integration "jumblednameitgenerated";
```

If the `SAML2_SSO_URL` or `SAML2_ISSUER` has test in it, it's probably the remnant, like below:

![image](https://user-images.githubusercontent.com/14199416/148827318-2ffeedb0-8fd1-4c38-a411-38b5f00c579a.png)

github-actions[bot]: Integration tests success for [7498921](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1679098930)
alldoami: yup I believe I deleted the one with the random string. Cool that the tests succeeded!
intinfinite: Awesome, thanks for putting up with me.  First time contributing to this project.
alldoami: Going to have @edulop91 do a passover, but in the meantime, can you pull in the most recent changes @intinfinite?
intinfinite: Sounds good. Merged from main.
alldoami: Hm still looks out of sync with `main`
intinfinite: All set
alldoami: /ok-to-test sha=995bc88
github-actions[bot]: Integration tests success for [995bc88](https://github.com/chanzuckerberg/terraform-provider-snowflake/actions/runs/1684191467)

Patch:
diff --git a/docs/resources/saml_integration.md b/docs/resources/saml_integration.md
new file mode 100644
index 00000000..abf8dd59
--- /dev/null
+++ b/docs/resources/saml_integration.md
@@ -0,0 +1,64 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "snowflake_saml_integration Resource - terraform-provider-snowflake"
+subcategory: ""
+description: |-
+  
+---
+
+# snowflake_saml_integration (Resource)
+
+
+
+## Example Usage
+
+```terraform
+resource "snowflake_saml_integration" "saml_integration" {
+  name = "saml_integration"
+  saml2_provider = "CUSTOM"
+  saml2_issuer = "test_issuer"
+  saml2_sso_url = "https://testsamlissuer.com"
+  saml2_x509_cert = "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO"
+  enabled = true
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- **name** (String) Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.
+- **saml2_issuer** (String) The string containing the IdP EntityID / Issuer.
+- **saml2_provider** (String) The string describing the IdP. One of the following: OKTA, ADFS, Custom.
+- **saml2_sso_url** (String) The string containing the IdP SSO URL, where the user should be redirected by Snowflake (the Service Provider) with a SAML AuthnRequest message.
+- **saml2_x509_cert** (String) The Base64 encoded IdP signing certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers.
+
+### Optional
+
+- **enabled** (Boolean) Specifies whether this security integration is enabled or disabled.
+- **id** (String) The ID of this resource.
+- **saml2_enable_sp_initiated** (Boolean) The Boolean indicating if the Log In With button will be shown on the login page. TRUE: displays the Log in WIth button on the login page.  FALSE: does not display the Log in With button on the login page.
+- **saml2_force_authn** (Boolean) The Boolean indicating whether users, during the initial authentication flow, are forced to authenticate again to access Snowflake. When set to TRUE, Snowflake sets the ForceAuthn SAML parameter to TRUE in the outgoing request from Snowflake to the identity provider. TRUE: forces users to authenticate again to access Snowflake, even if a valid session with the identity provider exists. FALSE: does not force users to authenticate again to access Snowflake.
+- **saml2_post_logout_redirect_url** (String) The endpoint to which Snowflake redirects users after clicking the Log Out button in the classic Snowflake web interface. Snowflake terminates the Snowflake session upon redirecting to the specified endpoint.
+- **saml2_requested_nameid_format** (String) The SAML NameID format allows Snowflake to set an expectation of the identifying attribute of the user (i.e. SAML Subject) in the SAML assertion from the IdP to ensure a valid authentication to Snowflake. If a value is not specified, Snowflake sends the urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress value in the authentication request to the IdP. NameID must be one of the following values: urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified, urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress, urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName, urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName, urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos, urn:oasis:names:tc:SAML:2.0:nameid-format:persistent, urn:oasis:names:tc:SAML:2.0:nameid-format:transient .
+- **saml2_sign_request** (Boolean) The Boolean indicating whether SAML requests are signed. TRUE: allows SAML requests to be signed. FALSE: does not allow SAML requests to be signed.
+- **saml2_snowflake_acs_url** (String) The string containing the Snowflake Assertion Consumer Service URL to which the IdP will send its SAML authentication response back to Snowflake. This property will be set in the SAML authentication request generated by Snowflake when initiating a SAML SSO operation with the IdP. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use. Default: https://<account_locator>.<region>.snowflakecomputing.com/fed/login
+- **saml2_snowflake_issuer_url** (String) The string containing the EntityID / Issuer for the Snowflake service provider. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use.
+- **saml2_snowflake_x509_cert** (String) The Base64 encoded self-signed certificate generated by Snowflake for use with Encrypting SAML Assertions and Signed SAML Requests. You must have at least one of these features (encrypted SAML assertions or signed SAML responses) enabled in your Snowflake account to access the certificate value.
+- **saml2_sp_initiated_login_page_label** (String) The string containing the label to display after the Log In With button on the login page.
+
+### Read-Only
+
+- **created_on** (String) Date and time when the SAML integration was created.
+- **saml2_digest_methods_used** (String)
+- **saml2_signature_methods_used** (String)
+- **saml2_snowflake_metadata** (String) Metadata created by Snowflake to provide to SAML2 provider.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import snowflake_saml_integration.example name
+```
diff --git a/examples/resources/snowflake_saml_integration/import.sh b/examples/resources/snowflake_saml_integration/import.sh
new file mode 100644
index 00000000..a2356ebb
--- /dev/null
+++ b/examples/resources/snowflake_saml_integration/import.sh
@@ -0,0 +1,1 @@
+terraform import snowflake_saml_integration.example name
\ No newline at end of file
diff --git a/examples/resources/snowflake_saml_integration/resource.tf b/examples/resources/snowflake_saml_integration/resource.tf
new file mode 100644
index 00000000..8193b785
--- /dev/null
+++ b/examples/resources/snowflake_saml_integration/resource.tf
@@ -0,0 +1,8 @@
+resource "snowflake_saml_integration" "saml_integration" {
+  name = "saml_integration"
+  saml2_provider = "CUSTOM"
+  saml2_issuer = "test_issuer"
+  saml2_sso_url = "https://testsamlissuer.com"
+  saml2_x509_cert = "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO"
+  enabled = true
+}
\ No newline at end of file
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index fd148bb1..9887691c 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -184,6 +184,7 @@ func getResources() map[string]*schema.Resource {
 		"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(),
diff --git a/pkg/resources/saml_integration.go b/pkg/resources/saml_integration.go
new file mode 100644
index 00000000..4b526c2c
--- /dev/null
+++ b/pkg/resources/saml_integration.go
@@ -0,0 +1,476 @@
+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 samlIntegrationSchema = map[string]*schema.Schema{
+	"name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		ForceNew:    true,
+		Description: "Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.",
+	},
+	"enabled": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Default:     true,
+		Description: "Specifies whether this security integration is enabled or disabled.",
+	},
+	"saml2_issuer": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The string containing the IdP EntityID / Issuer.",
+	},
+	"saml2_sso_url": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The string containing the IdP SSO URL, where the user should be redirected by Snowflake (the Service Provider) with a SAML AuthnRequest message.",
+	},
+	"saml2_provider": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The string describing the IdP. One of the following: OKTA, ADFS, Custom.",
+		ValidateFunc: validation.StringInSlice([]string{
+			"OKTA", "ADFS", "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)
+		},
+	},
+	"saml2_x509_cert": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The Base64 encoded IdP signing certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers.",
+	},
+	"saml2_sp_initiated_login_page_label": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "The string containing the label to display after the Log In With button on the login page.",
+	},
+	"saml2_enable_sp_initiated": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Description: "The Boolean indicating if the Log In With button will be shown on the login page. TRUE: displays the Log in WIth button on the login page.  FALSE: does not display the Log in With button on the login page.",
+	},
+	// Computed and Optionally Settable. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_SNOWFLAKE_METADATA)
+	"saml2_snowflake_x509_cert": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Computed:    true,
+		Description: "The Base64 encoded self-signed certificate generated by Snowflake for use with Encrypting SAML Assertions and Signed SAML Requests. You must have at least one of these features (encrypted SAML assertions or signed SAML responses) enabled in your Snowflake account to access the certificate value.",
+	},
+	"saml2_sign_request": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Description: "The Boolean indicating whether SAML requests are signed. TRUE: allows SAML requests to be signed. FALSE: does not allow SAML requests to be signed.",
+	},
+	"saml2_requested_nameid_format": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "The SAML NameID format allows Snowflake to set an expectation of the identifying attribute of the user (i.e. SAML Subject) in the SAML assertion from the IdP to ensure a valid authentication to Snowflake. If a value is not specified, Snowflake sends the urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress value in the authentication request to the IdP. NameID must be one of the following values: urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified, urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress, urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName, urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName, urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos, urn:oasis:names:tc:SAML:2.0:nameid-format:persistent, urn:oasis:names:tc:SAML:2.0:nameid-format:transient .",
+		ValidateFunc: validation.StringInSlice([]string{
+			"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
+			"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
+			"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
+			"urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
+			"urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos",
+			"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
+			"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
+		}, true),
+	},
+	"saml2_post_logout_redirect_url": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "The endpoint to which Snowflake redirects users after clicking the Log Out button in the classic Snowflake web interface. Snowflake terminates the Snowflake session upon redirecting to the specified endpoint.",
+	},
+	"saml2_force_authn": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Description: "The Boolean indicating whether users, during the initial authentication flow, are forced to authenticate again to access Snowflake. When set to TRUE, Snowflake sets the ForceAuthn SAML parameter to TRUE in the outgoing request from Snowflake to the identity provider. TRUE: forces users to authenticate again to access Snowflake, even if a valid session with the identity provider exists. FALSE: does not force users to authenticate again to access Snowflake.",
+	},
+	// Computed and Optionally Settable. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_SNOWFLAKE_METADATA)
+	"saml2_snowflake_issuer_url": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Computed:    true,
+		Description: "The string containing the EntityID / Issuer for the Snowflake service provider. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use.",
+	},
+	// Computed and Optionally Settable. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_SNOWFLAKE_METADATA)
+	"saml2_snowflake_acs_url": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Computed:    true,
+		Description: "The string containing the Snowflake Assertion Consumer Service URL to which the IdP will send its SAML authentication response back to Snowflake. This property will be set in the SAML authentication request generated by Snowflake when initiating a SAML SSO operation with the IdP. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use. Default: https://<account_locator>.<region>.snowflakecomputing.com/fed/login",
+	},
+	// Computed. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_SNOWFLAKE_METADATA)
+	"saml2_snowflake_metadata": {
+		Type:        schema.TypeString,
+		Computed:    true,
+		Description: "Metadata created by Snowflake to provide to SAML2 provider.",
+	},
+	// Computed. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_DIGEST_METHODS_USED)
+	"saml2_digest_methods_used": {
+		Type:     schema.TypeString,
+		Computed: true,
+	},
+	// Computed. Info you get by issuing a 'DESCRIBE INTEGRATION <name>' command (SAML2_SIGNATURE_METHODS_USED)
+	"saml2_signature_methods_used": {
+		Type:     schema.TypeString,
+		Computed: true,
+	},
+	"created_on": {
+		Type:        schema.TypeString,
+		Computed:    true,
+		Description: "Date and time when the SAML integration was created.",
+	},
+}
+
+// SAMLIntegration returns a pointer to the resource representing a SAML2 security integration
+func SAMLIntegration() *schema.Resource {
+	return &schema.Resource{
+		Create: CreateSAMLIntegration,
+		Read:   ReadSAMLIntegration,
+		Update: UpdateSAMLIntegration,
+		Delete: DeleteSAMLIntegration,
+
+		Schema: samlIntegrationSchema,
+		Importer: &schema.ResourceImporter{
+			StateContext: schema.ImportStatePassthroughContext,
+		},
+	}
+}
+
+// CreateSAMLIntegration implements schema.CreateFunc
+func CreateSAMLIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := d.Get("name").(string)
+
+	stmt := snowflake.SamlIntegration(name).Create()
+
+	// Set required fields
+	stmt.SetRaw(`TYPE=SAML2`)
+	stmt.SetBool(`ENABLED`, d.Get("enabled").(bool))
+	stmt.SetString(`SAML2_ISSUER`, d.Get("saml2_issuer").(string))
+	stmt.SetString(`SAML2_SSO_URL`, d.Get("saml2_sso_url").(string))
+	stmt.SetString(`SAML2_PROVIDER`, d.Get("saml2_provider").(string))
+
+	// Set optional fields
+	if _, ok := d.GetOk("saml2_x509_cert"); ok {
+		stmt.SetString(`SAML2_X509_CERT`, d.Get("saml2_x509_cert").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_sp_initiated_login_page_label"); ok {
+		stmt.SetString(`SAML2_SP_INITIATED_LOGIN_PAGE_LABEL`, d.Get("saml2_sp_initiated_login_page_label").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_enable_sp_initiated"); ok {
+		stmt.SetString(`SAML2_ENABLE_SP_INITIATED`, d.Get("saml2_enable_sp_initiated").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_snowflake_x509_cert"); ok {
+		stmt.SetString(`SAML2_SNOWFLAKE_X509_CERT`, d.Get("saml2_snowflake_x509_cert").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_sign_request"); ok {
+		stmt.SetString(`SAML2_SIGN_REQUEST`, d.Get("saml2_sign_request").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_requested_nameid_format"); ok {
+		stmt.SetString(`SAML2_REQUESTED_NAMEID_FORMAT`, d.Get("saml2_requested_nameid_format").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_post_logout_redirect_url"); ok {
+		stmt.SetString(`SAML2_POST_LOGOUT_REDIRECT_URL`, d.Get("saml2_post_logout_redirect_url").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_force_authn"); ok {
+		stmt.SetString(`SAML2_FORCE_AUTHN`, d.Get("saml2_force_authn").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_snowflake_issuer_url"); ok {
+		stmt.SetString(`SAML2_SNOWFLAKE_ISSUER_URL`, d.Get("saml2_snowflake_issuer_url").(string))
+	}
+
+	if _, ok := d.GetOk("saml2_snowflake_acs_url"); ok {
+		stmt.SetString(`SAML2_SNOWFLAKE_ACS_URL`, d.Get("saml2_snowflake_acs_url").(string))
+	}
+
+	err := snowflake.Exec(db, stmt.Statement())
+	if err != nil {
+		return errors.Wrap(err, "error creating security integration")
+	}
+
+	d.SetId(name)
+
+	return ReadSAMLIntegration(d, meta)
+}
+
+// ReadSAMLIntegration implements schema.ReadFunc
+func ReadSAMLIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	id := d.Id()
+
+	stmt := snowflake.SamlIntegration(id).Show()
+	row := snowflake.QueryRow(db, stmt)
+
+	// Some properties can come from the SHOW INTEGRATION call
+
+	s, err := snowflake.ScanSamlIntegration(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)
+	}
+
+	// Note: type must be SAML2 or something is broken
+	if c := s.IntegrationType.String; c != "SAML2" {
+		return fmt.Errorf("expected %v to be a SAML2 integration type, got %v", id, c)
+	}
+
+	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
+	}
+
+	if err := d.Set("enabled", s.Enabled.Bool); 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.SamlIntegration(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":
+			// set using the SHOW INTEGRATION, ignoring here
+		case "SAML2_ISSUER":
+			if err = d.Set("saml2_issuer", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_issuer for security integration")
+			}
+		case "SAML2_SSO_URL":
+			if err = d.Set("saml2_sso_url", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_sso_url for security integration")
+			}
+		case "SAML2_PROVIDER":
+			if err = d.Set("saml2_provider", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_provider for security integration")
+			}
+		case "SAML2_X509_CERT":
+			if err = d.Set("saml2_x509_cert", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_x509_cert for security integration")
+			}
+		case "SAML2_SP_INITIATED_LOGIN_PAGE_LABEL":
+			if err = d.Set("saml2_sp_initiated_login_page_label", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_sp_initiated_login_page_label for security integration")
+			}
+		case "SAML2_ENABLE_SP_INITIATED":
+			b := false
+			switch v2 := v.(type) {
+			case bool:
+				b = v2
+			case string:
+				b, err = strconv.ParseBool(v.(string))
+				if err != nil {
+					return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+				}
+			default:
+				return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+			}
+			if err = d.Set("saml2_enable_sp_initiated", b); err != nil {
+				return errors.Wrap(err, "unable to set saml2_enable_sp_initiated for security integration")
+			}
+		case "SAML2_SNOWFLAKE_X509_CERT":
+			if err = d.Set("saml2_snowflake_x509_cert", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_snowflake_x509_cert for security integration")
+			}
+		case "SAML2_SIGN_REQUEST":
+			b := false
+			switch v2 := v.(type) {
+			case bool:
+				b = v2
+			case string:
+				b, err = strconv.ParseBool(v.(string))
+				if err != nil {
+					return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+				}
+			default:
+				return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+			}
+			if err = d.Set("saml2_sign_request", b); err != nil {
+				return errors.Wrap(err, "unable to set saml2_sign_request for security integration")
+			}
+		case "SAML2_REQUESTED_NAMEID_FORMAT":
+			if err = d.Set("saml2_requested_nameid_format", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_requested_nameid_format for security integration")
+			}
+		case "SAML2_POST_LOGOUT_REDIRECT_URL":
+			if err = d.Set("saml2_post_logout_redirect_url", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_post_logout_redirect_url for security integration")
+			}
+		case "SAML2_FORCE_AUTHN":
+			b := false
+			switch v2 := v.(type) {
+			case bool:
+				b = v2
+			case string:
+				b, err = strconv.ParseBool(v.(string))
+				if err != nil {
+					return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+				}
+			default:
+				return errors.Wrap(err, "returned saml2_force_authn that is not boolean")
+			}
+			if err = d.Set("saml2_force_authn", b); err != nil {
+				return errors.Wrap(err, "unable to set saml2_force_authn for security integration")
+			}
+		case "SAML2_SNOWFLAKE_ISSUER_URL":
+			if err = d.Set("saml2_snowflake_issuer_url", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_snowflake_issuer_url for security integration")
+			}
+		case "SAML2_SNOWFLAKE_ACS_URL":
+			if err = d.Set("saml2_snowflake_acs_url", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_snowflake_acs_url for security integration")
+			}
+		case "SAML2_SNOWFLAKE_METADATA":
+			if err = d.Set("saml2_snowflake_metadata", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_snowflake_metadata for security integration")
+			}
+		case "SAML2_DIGEST_METHODS_USED":
+			if err = d.Set("saml2_digest_methods_used", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_digest_methods_used for security integration")
+			}
+		case "SAML2_SIGNATURE_METHODS_USED":
+			if err = d.Set("saml2_signature_methods_used", v.(string)); err != nil {
+				return errors.Wrap(err, "unable to set saml2_signature_methods_used for security integration")
+			}
+		case "COMMENT":
+			// COMMENT cannot be set according to snowflake docs, so ignoring
+		default:
+			log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k)
+		}
+	}
+
+	return err
+}
+
+// UpdateSAMLIntegration implements schema.UpdateFunc
+func UpdateSAMLIntegration(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	id := d.Id()
+
+	stmt := snowflake.SamlIntegration(id).Alter()
+
+	var runSetStatement bool
+
+	if d.HasChange("enabled") {
+		runSetStatement = true
+		stmt.SetBool(`ENABLED`, d.Get("enabled").(bool))
+	}
+
+	if d.HasChange("saml2_issuer") {
+		runSetStatement = true
+		stmt.SetString(`SAML2_ISSUER`, d.Get("saml2_issuer").(string))
+	}
+
+	if d.HasChange("saml2_sso_url") {
+		runSetStatement = true
+		stmt.SetString(`saml2_sso_url`, d.Get("saml2_sso_url").(string))
+	}
+
+	if d.HasChange("saml2_provider") {
+		runSetStatement = true
+		stmt.SetString(`saml2_provider`, d.Get("saml2_provider").(string))
+	}
+
+	if d.HasChange("saml2_x509_cert") {
+		runSetStatement = true
+		stmt.SetString(`saml2_x509_cert`, d.Get("saml2_x509_cert").(string))
+	}
+
+	if d.HasChange("saml2_sp_initiated_login_page_label") {
+		runSetStatement = true
+		stmt.SetString(`saml2_sp_initiated_login_page_label`, d.Get("saml2_sp_initiated_login_page_label").(string))
+	}
+
+	if d.HasChange("saml2_enable_sp_initiated") {
+		runSetStatement = true
+		stmt.SetBool(`saml2_enable_sp_initiated`, d.Get("saml2_enable_sp_initiated").(bool))
+	}
+
+	if d.HasChange("saml2_snowflake_x509_cert") {
+		runSetStatement = true
+		stmt.SetString(`saml2_snowflake_x509_cert`, d.Get("saml2_snowflake_x509_cert").(string))
+	}
+
+	if d.HasChange("saml2_sign_request") {
+		runSetStatement = true
+		stmt.SetBool(`saml2_sign_request`, d.Get("saml2_sign_request").(bool))
+	}
+
+	if d.HasChange("saml2_requested_nameid_format") {
+		runSetStatement = true
+		stmt.SetString(`saml2_requested_nameid_format`, d.Get("saml2_requested_nameid_format").(string))
+	}
+
+	if d.HasChange("saml2_post_logout_redirect_url") {
+		runSetStatement = true
+		stmt.SetString(`saml2_post_logout_redirect_url`, d.Get("saml2_post_logout_redirect_url").(string))
+	}
+
+	if d.HasChange("saml2_force_authn") {
+		runSetStatement = true
+		stmt.SetBool(`saml2_force_authn`, d.Get("saml2_force_authn").(bool))
+	}
+
+	if d.HasChange("saml2_snowflake_issuer_url") {
+		runSetStatement = true
+		stmt.SetString(`saml2_snowflake_issuer_url`, d.Get("saml2_snowflake_issuer_url").(string))
+	}
+
+	if d.HasChange("saml2_snowflake_acs_url") {
+		runSetStatement = true
+		stmt.SetString(`saml2_snowflake_acs_url`, d.Get("saml2_snowflake_acs_url").(string))
+	}
+
+	if runSetStatement {
+		if err := snowflake.Exec(db, stmt.Statement()); err != nil {
+			return errors.Wrap(err, "error updating security integration")
+		}
+	}
+
+	return ReadSAMLIntegration(d, meta)
+}
+
+// DeleteSAMLIntegration implements schema.DeleteFunc
+func DeleteSAMLIntegration(d *schema.ResourceData, meta interface{}) error {
+	return DeleteResource("", snowflake.SamlIntegration)(d, meta)
+}
diff --git a/pkg/snowflake/saml_integration.go b/pkg/snowflake/saml_integration.go
new file mode 100644
index 00000000..59d60f88
--- /dev/null
+++ b/pkg/snowflake/saml_integration.go
@@ -0,0 +1,38 @@
+package snowflake
+
+import (
+	"database/sql"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/pkg/errors"
+)
+
+// SamlIntegration returns a pointer to a Builder that abstracts the DDL operations for a SAML2 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 SamlIntegration(name string) *Builder {
+	return &Builder{
+		entityType: SecurityIntegrationType,
+		name:       name,
+	}
+}
+
+type samlIntegration struct {
+	Name            sql.NullString `db:"name"`
+	Category        sql.NullString `db:"category"`
+	IntegrationType sql.NullString `db:"type"`
+	CreatedOn       sql.NullString `db:"created_on"`
+	Enabled         sql.NullBool   `db:"enabled"`
+}
+
+func ScanSamlIntegration(row *sqlx.Row) (*samlIntegration, error) {
+	r := &samlIntegration{}
+	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 1e4b0d3d..28bcb115 100644
--- a/pkg/resources/helpers_test.go
+++ b/pkg/resources/helpers_test.go
@@ -200,6 +200,14 @@ func apiIntegration(t *testing.T, id string, params map[string]interface{}) *sch
 	return d
 }
 
+func samlIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
+	r := require.New(t)
+	d := schema.TestResourceDataRaw(t, resources.SAMLIntegration().Schema, params)
+	r.NotNil(d)
+	d.SetId(id)
+	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)
diff --git a/pkg/resources/saml_integration_acceptance_test.go b/pkg/resources/saml_integration_acceptance_test.go
new file mode 100644
index 00000000..bd712a20
--- /dev/null
+++ b/pkg/resources/saml_integration_acceptance_test.go
@@ -0,0 +1,55 @@
+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_SamlIntegration(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_SAML_INTEGRATION_TESTS"); ok {
+		t.Skip("Skipping TestAccSamlIntegration")
+	}
+
+	samlIntName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+
+	resource.Test(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: samlIntegrationConfig(samlIntName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_saml_integration.test_saml_int", "name", samlIntName),
+					resource.TestCheckResourceAttr("snowflake_saml_integration.test_saml_int", "saml2_issuer", "test_issuer"),
+					resource.TestCheckResourceAttr("snowflake_saml_integration.test_saml_int", "saml2_sso_url", "https://testsamlissuer.com"),
+					resource.TestCheckResourceAttr("snowflake_saml_integration.test_saml_int", "saml2_provider", "CUSTOM"),
+					resource.TestCheckResourceAttr("snowflake_saml_integration.test_saml_int", "saml2_x509_cert", "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "created_on"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_snowflake_x509_cert"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_snowflake_acs_url"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_snowflake_issuer_url"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_snowflake_metadata"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_digest_methods_used"),
+					resource.TestCheckResourceAttrSet("snowflake_saml_integration.test_saml_int", "saml2_signature_methods_used"),
+				),
+			},
+		},
+	})
+}
+
+func samlIntegrationConfig(name string) string {
+	return fmt.Sprintf(`
+	resource "snowflake_saml_integration" "test_saml_int" {
+		name = "%s"
+		saml2_issuer = "test_issuer"
+		saml2_sso_url = "https://testsamlissuer.com"
+		saml2_provider = "CUSTOM"
+		saml2_x509_cert = "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO"
+		enabled = false
+	}
+	`, name)
+}
diff --git a/pkg/resources/saml_integration_test.go b/pkg/resources/saml_integration_test.go
new file mode 100644
index 00000000..ab161ca5
--- /dev/null
+++ b/pkg/resources/saml_integration_test.go
@@ -0,0 +1,98 @@
+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 TestSAMLIntegration(t *testing.T) {
+	r := require.New(t)
+	err := resources.SAMLIntegration().InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestSAMLIntegrationCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":            "test_saml_integration",
+		"enabled":         true,
+		"saml2_issuer":    "test_issuer",
+		"saml2_sso_url":   "https://testsamlissuer.com",
+		"saml2_provider":  "CUSTOM",
+		"saml2_x509_cert": "MIICdummybase64certificate",
+	}
+	d := schema.TestResourceDataRaw(t, resources.SAMLIntegration().Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(
+			`^CREATE SECURITY INTEGRATION "test_saml_integration" TYPE=SAML2 SAML2_ISSUER='test_issuer' SAML2_PROVIDER='CUSTOM' SAML2_SSO_URL='https://testsamlissuer.com' SAML2_X509_CERT='MIICdummybase64certificate' ENABLED=true$`,
+		).WillReturnResult(sqlmock.NewResult(1, 1))
+		expectReadSAMLIntegration(mock)
+
+		err := resources.CreateSAMLIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestSAMLIntegrationRead(t *testing.T) {
+	r := require.New(t)
+
+	d := samlIntegration(t, "test_saml_integration", map[string]interface{}{"name": "test_saml_integration"})
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		expectReadSAMLIntegration(mock)
+
+		err := resources.ReadSAMLIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestSAMLIntegrationDelete(t *testing.T) {
+	r := require.New(t)
+
+	d := samlIntegration(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.DeleteSAMLIntegration(d, db)
+		r.NoError(err)
+	})
+}
+
+func expectReadSAMLIntegration(mock sqlmock.Sqlmock) {
+	showRows := sqlmock.NewRows([]string{
+		"name", "type", "category", "enabled", "created_on"},
+	).AddRow("test_saml_integration", "SAML2", "SECURITY", true, "now")
+	mock.ExpectQuery(`^SHOW SECURITY INTEGRATIONS LIKE 'test_saml_integration'$`).WillReturnRows(showRows)
+
+	descRows := sqlmock.NewRows([]string{
+		"property", "property_type", "property_value", "property_default",
+	}).AddRow("SAML2_X509_CERT", "String", "MIICdummybase64certificate", nil).
+		AddRow("SAML2_PROVIDER", "String", "CUSTOM", nil).
+		AddRow("SAML2_ENABLE_SP_INITIATED", "Boolean", false, false).
+		AddRow("SAML2_SP_INITIATED_LOGIN_PAGE_LABEL", "String", "MyLabel", nil).
+		AddRow("SAML2_SSO_URL", "String", "https://testsamlissuer.com", nil).
+		AddRow("SAML2_ISSUER", "String", "test_issuer", nil).
+		AddRow("SAML2_SNOWFLAKE_X509_CERT", "String", "MIICdummybase64certificate", nil).
+		AddRow("SAML2_REQUESTED_NAMEID_FORMAT", "String", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress").
+		AddRow("SAML2_FORCE_AUTHN", "Boolean", false, false).
+		AddRow("SAML2_POST_LOGOUT_REDIRECT_URL", "String", "https://myredirecturl.com", nil).
+		AddRow("SAML2_SIGN_REQUEST", "Boolean", false, false).
+		AddRow("SAML2_SNOWFLAKE_ACS_URL", "String", "https://myinstance.my-region-1.snowflakecomputing.com/fed/login", nil).
+		AddRow("SAML2_SNOWFLAKE_ISSUER_URL", "String", "https://myinstance.my-region-1.snowflakecomputing.com", nil).
+		AddRow("SAML2_SNOWFLAKE_METADATA", "String", "<md:EntityDescriptor...>", nil).
+		AddRow("SAML2_DIGEST_METHODS_USED", "http://www.w3.org/2001/04/xmlenc#sha256", "CUSTOM", nil).
+		AddRow("SAML2_SIGNATURE_METHODS_USED", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "CUSTOM", nil).
+		AddRow("COMMENT", "String", "Some Comment", nil)
+
+	mock.ExpectQuery(`DESCRIBE SECURITY INTEGRATION "test_saml_integration"$`).WillReturnRows(descRows)
+}
diff --git a/pkg/snowflake/saml_integration_test.go b/pkg/snowflake/saml_integration_test.go
new file mode 100644
index 00000000..afd59137
--- /dev/null
+++ b/pkg/snowflake/saml_integration_test.go
@@ -0,0 +1,43 @@
+package snowflake_test
+
+import (
+	"testing"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSamlIntegration(t *testing.T) {
+	r := require.New(t)
+	builder := snowflake.SamlIntegration("test_saml_integration")
+	r.NotNil(builder)
+
+	q := builder.Show()
+	r.Equal("SHOW SECURITY INTEGRATIONS LIKE 'test_saml_integration'", q)
+
+	q = builder.Describe()
+	r.Equal("DESCRIBE SECURITY INTEGRATION \"test_saml_integration\"", q)
+
+	c := builder.Create()
+	c.SetRaw(`TYPE=SAML2`)
+	c.SetString(`saml2_issuer`, "test_issuer")
+	c.SetString(`saml2_sso_url`, "https://testsamlissuer.com")
+	c.SetString(`saml2_provider`, "CUSTOM")
+	c.SetString(`saml2_x509_cert`, "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO")
+	c.SetBool(`enabled`, true)
+	q = c.Statement()
+	r.Equal(`CREATE SECURITY INTEGRATION "test_saml_integration" TYPE=SAML2 SAML2_ISSUER='test_issuer' SAML2_PROVIDER='CUSTOM' SAML2_SSO_URL='https://testsamlissuer.com' SAML2_X509_CERT='MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO' ENABLED=true`, q)
+
+	d := builder.Alter()
+	d.SetRaw(`TYPE=SAML2`)
+	d.SetString(`saml2_issuer`, "test_issuer")
+	d.SetString(`saml2_sso_url`, "https://testsamlissuer.com")
+	d.SetString(`saml2_provider`, "CUSTOM")
+	d.SetString(`saml2_x509_cert`, "MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO")
+	d.SetBool(`enabled`, false)
+	q = d.Statement()
+	r.Equal(`ALTER SECURITY INTEGRATION "test_saml_integration" SET TYPE=SAML2 SAML2_ISSUER='test_issuer' SAML2_PROVIDER='CUSTOM' SAML2_SSO_URL='https://testsamlissuer.com' SAML2_X509_CERT='MIICYzCCAcygAwIBAgIBADANBgkqhkiG9w0BAQUFADAuMQswCQYDVQQGEwJVUzEMMAoGA1UEChMDSUJNMREwDwYDVQQLEwhMb2NhbCBDQTAeFw05OTEyMjIwNTAwMDBaFw0wMDEyMjMwNDU5NTlaMC4xCzAJBgNVBAYTAlVTMQwwCgYDVQQKEwNJQk0xETAPBgNVBAsTCExvY2FsIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2bZEo7xGaX2/0GHkrNFZvlxBou9v1Jmt/PDiTMPve8r9FeJAQ0QdvFST/0JPQYD20rH0bimdDLgNdNynmyRoS2S/IInfpmf69iyc2G0TPyRvmHIiOZbdCd+YBHQi1adkj17NDcWj6S14tVurFX73zx0sNoMS79q3tuXKrDsxeuwIDAQABo4GQMIGNMEsGCVUdDwGG+EIBDQQ+EzxHZW5lcmF0ZWQgYnkgdGhlIFNlY3VyZVdheSBTZWN1cml0eSBTZXJ2ZXIgZm9yIE9TLzM5MCAoUkFDRikwDgYDVR0PAQH/BAQDAgAGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ3+ocRyCTJw067dLSwr/nalx6YMMA0GCSqGSIb3DQEBBQUAA4GBAMaQzt+zaj1GU77yzlr8iiMBXgdQrwsZZWJo5exnAucJAEYQZmOfyLiMD6oYq+ZnfvM0n8G/Y79q8nhwvuxpYOnRSAXFp6xSkrIOeZtJMY1h00LKp/JX3Ng1svZ2agE126JHsQ0bhzN5TKsYfbwfTwfjdWAGy6Vf1nYi/rO+ryMO' ENABLED=false`, q)
+
+	e := builder.Drop()
+	r.Equal(`DROP SECURITY INTEGRATION "test_saml_integration"`, e)
+}