From 1deaedc95df9b82229256055a4803387098c58d9 Mon Sep 17 00:00:00 2001 From: Artur Sawicki Date: Thu, 4 Jul 2024 12:20:13 +0200 Subject: [PATCH 1/2] fix: Add disclaimers and fix tests (#2905) - add disclaimers to the V1 release candidate resources and datasources - fix a few recently failing tests - update the essential progress chart --- docs/data-sources/databases.md | 2 ++ docs/data-sources/security_integrations.md | 2 ++ docs/data-sources/warehouses.md | 2 ++ docs/resources/database.md | 2 ++ docs/resources/scim_integration.md | 2 ++ docs/resources/secondary_database.md | 2 ++ docs/resources/shared_database.md | 2 ++ docs/resources/warehouse.md | 2 ++ pkg/datasources/alerts_acceptance_test.go | 1 - pkg/resources/stage_acceptance_test.go | 3 +- pkg/sdk/testint/grants_integration_test.go | 2 +- .../testint/warehouses_integration_test.go | 4 +-- templates/data-sources/databases.md.tmpl | 24 ++++++++++++++ .../security_integrations.md.tmpl | 24 ++++++++++++++ templates/data-sources/warehouses.md.tmpl | 24 ++++++++++++++ templates/resources/database.md.tmpl | 32 +++++++++++++++++++ templates/resources/scim_integration.md.tmpl | 32 +++++++++++++++++++ .../resources/secondary_database.md.tmpl | 2 ++ templates/resources/shared_database.md.tmpl | 32 +++++++++++++++++++ templates/resources/warehouse.md.tmpl | 32 +++++++++++++++++++ v1-preparations/ESSENTIAL_GA_OBJECTS.MD | 17 +++++----- 21 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 templates/data-sources/databases.md.tmpl create mode 100644 templates/data-sources/security_integrations.md.tmpl create mode 100644 templates/data-sources/warehouses.md.tmpl create mode 100644 templates/resources/database.md.tmpl create mode 100644 templates/resources/scim_integration.md.tmpl create mode 100644 templates/resources/shared_database.md.tmpl create mode 100644 templates/resources/warehouse.md.tmpl diff --git a/docs/data-sources/databases.md b/docs/data-sources/databases.md index 8de06ace20..4a501293cc 100644 --- a/docs/data-sources/databases.md +++ b/docs/data-sources/databases.md @@ -5,6 +5,8 @@ description: |- Datasource used to get details of filtered databases. Filtering is aligned with the current possibilities for SHOW DATABASES https://docs.snowflake.com/en/sql-reference/sql/show-databases query (like, starts_with, and limit are all supported). The results of SHOW, DESCRIBE, and SHOW PARAMETERS IN are encapsulated in one output collection. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_databases (Data Source) Datasource used to get details of filtered databases. Filtering is aligned with the current possibilities for [SHOW DATABASES](https://docs.snowflake.com/en/sql-reference/sql/show-databases) query (`like`, `starts_with`, and `limit` are all supported). The results of SHOW, DESCRIBE, and SHOW PARAMETERS IN are encapsulated in one output collection. diff --git a/docs/data-sources/security_integrations.md b/docs/data-sources/security_integrations.md index e3423a59bf..225a2f8703 100644 --- a/docs/data-sources/security_integrations.md +++ b/docs/data-sources/security_integrations.md @@ -5,6 +5,8 @@ description: |- Datasource used to get details of filtered security integrations. Filtering is aligned with the current possibilities for SHOW SECURITY INTEGRATIONS https://docs.snowflake.com/en/sql-reference/sql/show-integrations query (only like is supported). The results of SHOW and DESCRIBE are encapsulated in one output collection security_integrations. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_security_integrations (Data Source) Datasource used to get details of filtered security integrations. Filtering is aligned with the current possibilities for [SHOW SECURITY INTEGRATIONS](https://docs.snowflake.com/en/sql-reference/sql/show-integrations) query (only `like` is supported). The results of SHOW and DESCRIBE are encapsulated in one output collection `security_integrations`. diff --git a/docs/data-sources/warehouses.md b/docs/data-sources/warehouses.md index 6c548f36a8..dcfef526c0 100644 --- a/docs/data-sources/warehouses.md +++ b/docs/data-sources/warehouses.md @@ -5,6 +5,8 @@ description: |- Datasource used to get details of filtered warehouses. Filtering is aligned with the current possibilities for SHOW WAREHOUSES https://docs.snowflake.com/en/sql-reference/sql/show-warehouses query (only like is supported). The results of SHOW, DESCRIBE, and SHOW PARAMETERS IN are encapsulated in one output collection. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_warehouses (Data Source) Datasource used to get details of filtered warehouses. Filtering is aligned with the current possibilities for [SHOW WAREHOUSES](https://docs.snowflake.com/en/sql-reference/sql/show-warehouses) query (only `like` is supported). The results of SHOW, DESCRIBE, and SHOW PARAMETERS IN are encapsulated in one output collection. diff --git a/docs/resources/database.md b/docs/resources/database.md index d9b2a07cc2..abd0644f13 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -5,6 +5,8 @@ description: |- Represents a standard database. If replication configuration is specified, the database is promoted to serve as a primary database for replication. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_database (Resource) Represents a standard database. If replication configuration is specified, the database is promoted to serve as a primary database for replication. diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md index e4a0dd1086..2b5f656711 100644 --- a/docs/resources/scim_integration.md +++ b/docs/resources/scim_integration.md @@ -5,6 +5,8 @@ description: |- --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_scim_integration (Resource) diff --git a/docs/resources/secondary_database.md b/docs/resources/secondary_database.md index d3948e8037..4a163b2cab 100644 --- a/docs/resources/secondary_database.md +++ b/docs/resources/secondary_database.md @@ -6,6 +6,8 @@ description: |- A secondary database creates a replica of an existing primary database (i.e. a secondary database). For more information about database replication, see Introduction to database replication across multiple accounts https://docs.snowflake.com/en/user-guide/db-replication-intro. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_secondary_database (Resource) ~> **Note** The snowflake_secondary_database resource doesn't refresh itself, as the best practice is to use tasks scheduled for a certain interval. Check out the examples to see how to set up the refresh task. For SQL-based replication guide, see the [official documentation](https://docs.snowflake.com/en/user-guide/db-replication-config#replicating-a-database-to-another-account). diff --git a/docs/resources/shared_database.md b/docs/resources/shared_database.md index d9b899dba0..6563fb2ccd 100644 --- a/docs/resources/shared_database.md +++ b/docs/resources/shared_database.md @@ -5,6 +5,8 @@ description: |- A shared database creates a database from a share provided by another Snowflake account. For more information about shares, see Introduction to Secure Data Sharing https://docs.snowflake.com/en/user-guide/data-sharing-intro. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_shared_database (Resource) A shared database creates a database from a share provided by another Snowflake account. For more information about shares, see [Introduction to Secure Data Sharing](https://docs.snowflake.com/en/user-guide/data-sharing-intro). diff --git a/docs/resources/warehouse.md b/docs/resources/warehouse.md index 3f62256b2d..b6d73ac2c9 100644 --- a/docs/resources/warehouse.md +++ b/docs/resources/warehouse.md @@ -5,6 +5,8 @@ description: |- Resource used to manage warehouse objects. For more information, check warehouse documentation https://docs.snowflake.com/en/sql-reference/commands-warehouse. --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # snowflake_warehouse (Resource) Resource used to manage warehouse objects. For more information, check [warehouse documentation](https://docs.snowflake.com/en/sql-reference/commands-warehouse). diff --git a/pkg/datasources/alerts_acceptance_test.go b/pkg/datasources/alerts_acceptance_test.go index ffa67019b2..9b2b023fd7 100644 --- a/pkg/datasources/alerts_acceptance_test.go +++ b/pkg/datasources/alerts_acceptance_test.go @@ -38,7 +38,6 @@ func TestAcc_Alerts(t *testing.T) { Config: alertsResourceConfig(alertId) + alertsDatasourceConfigDbAndSchema(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("data.snowflake_alerts.test_datasource_alert", "alerts.#"), - resource.TestCheckResourceAttr("data.snowflake_alerts.test_datasource_alert", "alerts.0.name", alertId.Name()), ), }, { diff --git a/pkg/resources/stage_acceptance_test.go b/pkg/resources/stage_acceptance_test.go index 58ac1ff4b8..f734f1de92 100644 --- a/pkg/resources/stage_acceptance_test.go +++ b/pkg/resources/stage_acceptance_test.go @@ -65,6 +65,7 @@ func TestAcc_Stage_CreateAndAlter(t *testing.T) { changedEncryption := "TYPE = 'AWS_SSE_S3'" changedFileFormat := "TYPE = JSON NULL_IF = []" changedFileFormatWithQuotes := "FIELD_DELIMITER = '|' PARSE_HEADER = true" + changedFileFormatWithoutQuotes := "FIELD_DELIMITER = | PARSE_HEADER = true" changedComment := random.Comment() copyOptionsWithoutQuotes := "ON_ERROR = CONTINUE" @@ -140,7 +141,7 @@ func TestAcc_Stage_CreateAndAlter(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "storage_integration", changedStorageIntegration.Name()), resource.TestCheckResourceAttr(resourceName, "credentials", credentials), resource.TestCheckResourceAttr(resourceName, "encryption", changedEncryption), - resource.TestCheckResourceAttr(resourceName, "file_format", changedFileFormatWithQuotes), + resource.TestCheckResourceAttr(resourceName, "file_format", changedFileFormatWithoutQuotes), resource.TestCheckResourceAttr(resourceName, "copy_options", copyOptionsWithoutQuotes), resource.TestCheckResourceAttr(resourceName, "url", changedUrl), resource.TestCheckResourceAttr(resourceName, "comment", changedComment), diff --git a/pkg/sdk/testint/grants_integration_test.go b/pkg/sdk/testint/grants_integration_test.go index 7cded5ead3..cff2094f81 100644 --- a/pkg/sdk/testint/grants_integration_test.go +++ b/pkg/sdk/testint/grants_integration_test.go @@ -238,7 +238,7 @@ func TestInt_GrantAndRevokePrivilegesToAccountRole(t *testing.T) { assert.Equal(t, 0, len(grants)) }) - t.Run("on schema object: cortex search service", func(t *testing.T) { + t.Run("on all: cortex search service", func(t *testing.T) { roleTest, roleCleanup := testClientHelper().Role.CreateRole(t) t.Cleanup(roleCleanup) diff --git a/pkg/sdk/testint/warehouses_integration_test.go b/pkg/sdk/testint/warehouses_integration_test.go index 953c11e822..08f84091d5 100644 --- a/pkg/sdk/testint/warehouses_integration_test.go +++ b/pkg/sdk/testint/warehouses_integration_test.go @@ -308,7 +308,7 @@ func TestInt_Warehouses(t *testing.T) { returnedWarehouse, err = client.Warehouses.ShowByID(ctx, warehouse.ID()) require.NoError(t, err) assert.Equal(t, sdk.WarehouseTypeSnowparkOptimized, returnedWarehouse.Type) - assert.Equal(t, sdk.WarehouseStateStarted, returnedWarehouse.State) + assert.Contains(t, []any{sdk.WarehouseStateStarted, sdk.WarehouseStateResuming}, returnedWarehouse.State) // TODO [SNOW-1473453]: uncomment and test when UNSET starts working correctly (expecting to unset to default type STANDARD) // alterOptions = &sdk.AlterWarehouseOptions{ @@ -335,7 +335,7 @@ func TestInt_Warehouses(t *testing.T) { returnedWarehouse, err := client.Warehouses.ShowByID(ctx, warehouse.ID()) require.NoError(t, err) assert.Equal(t, sdk.WarehouseTypeStandard, returnedWarehouse.Type) - assert.Equal(t, sdk.WarehouseStateSuspended, returnedWarehouse.State) + assert.Contains(t, []any{sdk.WarehouseStateSuspended, sdk.WarehouseStateSuspending}, returnedWarehouse.State) alterOptions := &sdk.AlterWarehouseOptions{ Set: &sdk.WarehouseSet{WarehouseType: sdk.Pointer(sdk.WarehouseTypeSnowparkOptimized)}, diff --git a/templates/data-sources/databases.md.tmpl b/templates/data-sources/databases.md.tmpl new file mode 100644 index 0000000000..18e5fffd7a --- /dev/null +++ b/templates/data-sources/databases.md.tmpl @@ -0,0 +1,24 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/data-sources/%s/data-source.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/data-sources/security_integrations.md.tmpl b/templates/data-sources/security_integrations.md.tmpl new file mode 100644 index 0000000000..18e5fffd7a --- /dev/null +++ b/templates/data-sources/security_integrations.md.tmpl @@ -0,0 +1,24 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/data-sources/%s/data-source.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/data-sources/warehouses.md.tmpl b/templates/data-sources/warehouses.md.tmpl new file mode 100644 index 0000000000..18e5fffd7a --- /dev/null +++ b/templates/data-sources/warehouses.md.tmpl @@ -0,0 +1,24 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/data-sources/%s/data-source.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/database.md.tmpl b/templates/resources/database.md.tmpl new file mode 100644 index 0000000000..e7f66bbf91 --- /dev/null +++ b/templates/resources/database.md.tmpl @@ -0,0 +1,32 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/scim_integration.md.tmpl b/templates/resources/scim_integration.md.tmpl new file mode 100644 index 0000000000..e7f66bbf91 --- /dev/null +++ b/templates/resources/scim_integration.md.tmpl @@ -0,0 +1,32 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/secondary_database.md.tmpl b/templates/resources/secondary_database.md.tmpl index 2fb8f72c98..cd1c7f9eeb 100644 --- a/templates/resources/secondary_database.md.tmpl +++ b/templates/resources/secondary_database.md.tmpl @@ -10,6 +10,8 @@ description: |- {{- end }} --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + # {{.Name}} ({{.Type}}) ~> **Note** The snowflake_secondary_database resource doesn't refresh itself, as the best practice is to use tasks scheduled for a certain interval. Check out the examples to see how to set up the refresh task. For SQL-based replication guide, see the [official documentation](https://docs.snowflake.com/en/user-guide/db-replication-config#replicating-a-database-to-another-account). diff --git a/templates/resources/shared_database.md.tmpl b/templates/resources/shared_database.md.tmpl new file mode 100644 index 0000000000..e7f66bbf91 --- /dev/null +++ b/templates/resources/shared_database.md.tmpl @@ -0,0 +1,32 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/warehouse.md.tmpl b/templates/resources/warehouse.md.tmpl new file mode 100644 index 0000000000..e7f66bbf91 --- /dev/null +++ b/templates/resources/warehouse.md.tmpl @@ -0,0 +1,32 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD index d175228f57..dc9858cbcc 100644 --- a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD +++ b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD @@ -4,31 +4,32 @@ This table represents the current state of preparing the essential objects for t Status is one of: - ✅ - done
+- 🚀 - V1 release candidate
- ❌ - not started
- 👨‍💻 - in progress
Known issues lists open issues touching the given object. Note that some of these issues may be already fixed in the -newer provider versions. We will address these while working on the given object. +newer provider versions. We will address these while working on the given object. | Object Type | Status | Known issues | |--------------------------|:------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ACCOUNT | ❌ | [#2030](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2030), [#2015](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2015), [#1891](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1891), [#1679](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1679), [#1671](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1671), [#1501](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1501), [#1062](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1062) | -| DATABASE | 👨‍💻 | [#2590](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2590), [#2321](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2321), [#2277](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2277), [#1833](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1833), [#1770](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1770), [#1453](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1453), [#1371](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1371), [#1367](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1367), [#1045](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1045), [#506](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/506) | +| DATABASE | 🚀 | [#2590](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2590), [#2321](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2321), [#2277](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2277), [#1833](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1833), [#1770](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1770), [#1453](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1453), [#1371](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1371), [#1367](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1367), [#1045](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1045), [#506](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/506) | | DATABASE ROLE | ❌ | - | -| NETWORK POLICY | ❌ | - | +| NETWORK POLICY | 👨‍💻 | - | | RESOURCE MONITOR | ❌ | [#1990](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1990), [#1832](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1832), [#1821](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1821), [#1754](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1754), [#1716](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1716), [#1714](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1714), [#1624](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1624), [#1500](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1500), [#1175](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1175) | -| ROLE | ❌ | - | +| ROLE | 👨‍💻 | - | | SECURITY INTEGRATION | 👨‍💻 | [#2719](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2719), [#2568](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2568), [#2177](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2177), [#1851](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1851), [#1773](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1773), [#1741](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1741), [#1637](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1637), [#1503](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1503), [#1498](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1498), [#1421](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1421), [#1224](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1224) | -| USER | ❌ | [#2817](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2817), [#2662](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2662), [#1572](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1572), [#1535](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1535), [#1155](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1155) | -| WAREHOUSE | 👨‍💻 | [#1844](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1844), [#1104](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1104) | +| USER | 👨‍💻 | [#2817](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2817), [#2662](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2662), [#1572](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1572), [#1535](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1535), [#1155](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1155) | +| WAREHOUSE | 🚀 | issues in the older versions: [resources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Aresource%3Awarehouse+) and [datasources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Adata_source%3Awarehouses+) | | FUNCTION | ❌ | [2859](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2859), [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2426](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2426), [#1479](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1479), [#1393](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1393), [#1208](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1208), [#1079](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1079) | | MASKING POLICY | ❌ | [#2236](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2236), [#2035](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2035), [#1799](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1799), [#1764](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1764), [#1656](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1656), [#1444](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1444), [#1422](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1422), [#1097](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1097) | | PROCEDURE | ❌ | [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2623](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2623), [#2257](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2257), [#2146](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2146), [#1855](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1855), [#1695](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1695), [#1640](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1640), [#1195](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1195), [#1189](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1189), [#1178](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1178), [#1050](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1050) | | ROW ACCESS POLICY | ❌ | [#2053](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053), [#1600](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1600), [#1151](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1151) | -| SCHEMA | ❌ | [#2826](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2826), [#2211](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2211), [#1243](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1243), [#506](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/506) | +| SCHEMA | 👨‍💻 | [#2826](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2826), [#2211](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2211), [#1243](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1243), [#506](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/506) | | STAGE | ❌ | [#2818](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2818), [#2505](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2505), [#1911](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1911), [#1903](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1903), [#1795](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1795), [#1705](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1705), [#1544](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1544), [#1491](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1491), [#1087](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1087), [#265](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/265) | | STREAM | ❌ | [#2413](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2413), [#2201](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2201), [#1150](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1150) | -| STREAMLIT | ❌ | [#1933](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1933) | +| STREAMLIT | 👨‍💻 | [#1933](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1933) | | TABLE | ❌ | [#2844](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2844). [#2839](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2839), [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2733](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2733), [#2683](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2683), [#2676](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2676), [#2674](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2674), [#2629](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2629), [#2418](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2418), [#2415](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2415), [#2406](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2406), [#2236](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2236), [#2035](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2035), [#1799](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1799), [#1764](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1764), [#1600](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1600), [#1387](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1387), [#1272](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1272), [#1271](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1271), [#1248](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1248), [#1241](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1241), [#1146](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1146), [#1032](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1032), [#420](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/420) | | TAG | ❌ | [#2598](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2598), [#1910](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1910), [#1909](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1909), [#1862](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1862), [#1806](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1806), [#1657](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1657), [#1496](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1496), [#1443](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1443), [#1394](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1394), [#1372](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1372), [#1074](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1074) | | TASK | ❌ | [#1419](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1419), [#1250](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1250), [#1194](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1194), [#1088](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1088) | From d0c136d1e669ee97b76199ce1e906b469b279842 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 4 Jul 2024 15:38:47 +0200 Subject: [PATCH 2/2] feat: SAML2 integration v1 readiness (#2868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `snowflake_saml2_resource` (to be consistent with docs and sql) and deprecate `snowflake_saml_resource` - Fix migration guide - Fix checking errors for invalid terraform configurations - Added one more test for scim migrator - Altered SDK: - improved enums - marked some fields as optional - introduced enums for some fields (they were not here before because of confusing docs, added to docs improvements list) - added new issues to objects lists ## Test Plan * [x] acceptance tests * [ ] … ## References https://docs.snowflake.com/en/sql-reference/sql/create-security-integration-saml2 --------- Co-authored-by: Jan Cieślak --- MIGRATION_GUIDE.md | 2 +- docs/index.md | 1 + docs/resources/saml2_integration.md | 332 ++++++ docs/resources/saml_integration.md | 2 +- docs/resources/scim_integration.md | 2 +- examples/additional/deprecated_resources.MD | 1 + .../snowflake_saml2_integration/import.sh | 1 + .../snowflake_saml2_integration/resource.tf | 30 + .../snowflake_scim_integration/import.sh | 2 +- pkg/acceptance/check_destroy.go | 3 + pkg/acceptance/helpers/common.go | 11 - pkg/acceptance/helpers/common_test.go | 58 - pkg/acceptance/helpers/random/certs.go | 14 +- .../helpers/security_integration_client.go | 15 +- pkg/acceptance/importchecks/import_checks.go | 53 + pkg/provider/provider.go | 1 + pkg/provider/resources/resources.go | 1 + pkg/resources/custom_diffs.go | 24 + pkg/resources/custom_diffs_test.go | 214 +++- pkg/resources/helpers_test.go | 130 -- pkg/resources/saml2_integration.go | 849 +++++++++++++ .../saml2_integration_acceptance_test.go | 1048 +++++++++++++++++ pkg/resources/saml_integration.go | 9 +- pkg/resources/scim_integration.go | 17 +- .../scim_integration_acceptance_test.go | 92 +- .../scim_integration_state_upgraders.go | 2 +- .../TestAcc_Saml2Integration/basic/test.tf | 7 + .../basic/variables.tf | 15 + .../TestAcc_Saml2Integration/complete/test.tf | 19 + .../complete/variables.tf | 52 + .../TestAcc_Saml2Integration/invalid/test.tf | 8 + .../invalid/variables.tf | 18 + .../recreates/test.tf | 12 + .../recreates/variables.tf | 30 + .../zero_values/test.tf | 11 + .../zero_values/variables.tf | 15 + pkg/schemas/saml2_security_integration.go | 64 + pkg/sdk/parsers.go | 22 + pkg/sdk/parsers_test.go | 57 + pkg/sdk/security_integrations_def.go | 165 ++- .../security_integrations_dto_builders_gen.go | 15 +- pkg/sdk/security_integrations_dto_gen.go | 16 +- pkg/sdk/security_integrations_gen.go | 80 +- pkg/sdk/security_integrations_gen_test.go | 20 +- ...urity_integrations_gen_integration_test.go | 48 +- v1-preparations/ESSENTIAL_GA_OBJECTS.MD | 4 +- 46 files changed, 3245 insertions(+), 347 deletions(-) create mode 100644 docs/resources/saml2_integration.md create mode 100644 examples/resources/snowflake_saml2_integration/import.sh create mode 100644 examples/resources/snowflake_saml2_integration/resource.tf delete mode 100644 pkg/acceptance/helpers/common_test.go create mode 100644 pkg/resources/saml2_integration.go create mode 100644 pkg/resources/saml2_integration_acceptance_test.go create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/basic/test.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/basic/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/complete/test.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/complete/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/invalid/test.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/invalid/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/recreates/test.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/recreates/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/zero_values/test.tf create mode 100644 pkg/resources/testdata/TestAcc_Saml2Integration/zero_values/variables.tf create mode 100644 pkg/schemas/saml2_security_integration.go create mode 100644 pkg/sdk/parsers_test.go diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 2e72424d20..bbd93e1eeb 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -49,7 +49,7 @@ Fields added to the resource: New field `enabled` is required. Previously the default value during create in Snowflake was `true`. If you created a resource with Terraform, please add `enabled = true` to have the same value. #### *(behavior change)* Force new for multiple attributes -Force new was added for the following attributes (because no usable SQL alter statements for them): +Force new was added for the following attributes (because there are no usable SQL alter statements for them): - `scim_client` - `run_as_role` diff --git a/docs/index.md b/docs/index.md index a2e8c0ec88..6ec1a994f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -230,6 +230,7 @@ The Snowflake provider will use the following order of precedence when determini ## Currently deprecated resources - [snowflake_database_old](./docs/resources/database_old) +- [snowflake_saml_integration](./docs/resources/saml_integration) - use [snowflake_saml2_integration](./docs/resources/saml2_integration) instead - [snowflake_unsafe_execute](./docs/resources/unsafe_execute) ## Currently deprecated datasources diff --git a/docs/resources/saml2_integration.md b/docs/resources/saml2_integration.md new file mode 100644 index 0000000000..998bfab775 --- /dev/null +++ b/docs/resources/saml2_integration.md @@ -0,0 +1,332 @@ +--- +page_title: "snowflake_saml2_integration Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_saml2_integration (Resource) + + + +## Example Usage + +```terraform +# basic resource +# each pem file contains a base64 encoded IdP signing certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers. +resource "snowflake_saml2_integration" "saml_integration" { + name = "saml_integration" + saml2_provider = "CUSTOM" + saml2_issuer = "test_issuer" + saml2_sso_url = "https://example.com" + saml2_x509_cert = file("cert.pem") +} +# resource with all fields set +resource "snowflake_saml2_integration" "test" { + allowed_email_patterns = ["^(.+dev)@example.com$"] + allowed_user_domains = ["example.com"] + comment = "foo" + enabled = true + name = "saml_integration" + saml2_enable_sp_initiated = true + saml2_force_authn = true + saml2_issuer = "foo" + saml2_post_logout_redirect_url = "https://example.com" + saml2_provider = "CUSTOM" + saml2_requested_nameid_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + saml2_sign_request = true + saml2_snowflake_acs_url = "example.snowflakecomputing.com/fed/login" + saml2_snowflake_issuer_url = "example.snowflakecomputing.com/fed/login" + saml2_snowflake_x509_cert = file("snowflake_cert.pem") + saml2_sp_initiated_login_page_label = "foo" + saml2_sso_url = "https://example.com" + saml2_x509_cert = file("cert.pem") +} +``` + + +## 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. Valid options are: [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 + +- `allowed_email_patterns` (Set of String) A list of regular expressions that email addresses are matched against to authenticate with a SAML2 security integration. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations. +- `allowed_user_domains` (Set of String) A list of email domains that can authenticate with a SAML2 security integration. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations. +- `comment` (String) Specifies a comment for the integration. +- `enabled` (String) Specifies whether this security integration is enabled or disabled. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value. +- `saml2_enable_sp_initiated` (String) 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. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value. +- `saml2_force_authn` (String) 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. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value. +- `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. Valid options are: [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` (String) The Boolean indicating whether SAML requests are signed. TRUE: allows SAML requests to be signed. FALSE: does not allow SAML requests to be signed. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value. +- `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. +- `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_sp_initiated_login_page_label` (String) The string containing the label to display after the Log In With button on the login page. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECURITY INTEGRATION` for the given integration. (see [below for nested schema](#nestedatt--describe_output)) +- `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW SECURITY INTEGRATION` for the given integration. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `allowed_email_patterns` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--allowed_email_patterns)) +- `allowed_user_domains` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--allowed_user_domains)) +- `comment` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--comment)) +- `saml2_digest_methods_used` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_digest_methods_used)) +- `saml2_enable_sp_initiated` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_enable_sp_initiated)) +- `saml2_force_authn` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_force_authn)) +- `saml2_issuer` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_issuer)) +- `saml2_post_logout_redirect_url` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_post_logout_redirect_url)) +- `saml2_provider` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_provider)) +- `saml2_requested_nameid_format` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_requested_nameid_format)) +- `saml2_sign_request` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_sign_request)) +- `saml2_signature_methods_used` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_signature_methods_used)) +- `saml2_snowflake_acs_url` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_snowflake_acs_url)) +- `saml2_snowflake_issuer_url` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_snowflake_issuer_url)) +- `saml2_snowflake_metadata` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_snowflake_metadata)) +- `saml2_snowflake_x509_cert` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_snowflake_x509_cert)) +- `saml2_sp_initiated_login_page_label` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_sp_initiated_login_page_label)) +- `saml2_sso_url` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_sso_url)) +- `saml2_x509_cert` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--saml2_x509_cert)) + + +### Nested Schema for `describe_output.allowed_email_patterns` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.allowed_user_domains` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.comment` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_digest_methods_used` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_enable_sp_initiated` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_force_authn` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_issuer` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_post_logout_redirect_url` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_provider` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_requested_nameid_format` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_sign_request` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_signature_methods_used` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_snowflake_acs_url` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_snowflake_issuer_url` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_snowflake_metadata` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_snowflake_x509_cert` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_sp_initiated_login_page_label` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_sso_url` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.saml2_x509_cert` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + + +### Nested Schema for `show_output` + +Read-Only: + +- `category` (String) +- `comment` (String) +- `created_on` (String) +- `enabled` (Boolean) +- `integration_type` (String) +- `name` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_saml2_integration.example "name" +``` diff --git a/docs/resources/saml_integration.md b/docs/resources/saml_integration.md index 05accb3cfe..ccc5d494f7 100644 --- a/docs/resources/saml_integration.md +++ b/docs/resources/saml_integration.md @@ -7,7 +7,7 @@ description: |- # snowflake_saml_integration (Resource) - +~> **Deprecation** This resource is deprecated and will be removed in a future major version release. Please use [snowflake_saml2_integration](./saml2_integration) instead. ## Example Usage diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md index 2b5f656711..43278d71f0 100644 --- a/docs/resources/scim_integration.md +++ b/docs/resources/scim_integration.md @@ -139,5 +139,5 @@ Read-Only: Import is supported using the following syntax: ```shell -terraform import snowflake_scim_integration.example name +terraform import snowflake_scim_integration.example "name" ``` diff --git a/examples/additional/deprecated_resources.MD b/examples/additional/deprecated_resources.MD index 979b77f0fd..c0e66e2d2d 100644 --- a/examples/additional/deprecated_resources.MD +++ b/examples/additional/deprecated_resources.MD @@ -1,4 +1,5 @@ ## Currently deprecated resources - [snowflake_database_old](./docs/resources/database_old) +- [snowflake_saml_integration](./docs/resources/saml_integration) - use [snowflake_saml2_integration](./docs/resources/saml2_integration) instead - [snowflake_unsafe_execute](./docs/resources/unsafe_execute) diff --git a/examples/resources/snowflake_saml2_integration/import.sh b/examples/resources/snowflake_saml2_integration/import.sh new file mode 100644 index 0000000000..bf68b01c98 --- /dev/null +++ b/examples/resources/snowflake_saml2_integration/import.sh @@ -0,0 +1 @@ +terraform import snowflake_saml2_integration.example "name" diff --git a/examples/resources/snowflake_saml2_integration/resource.tf b/examples/resources/snowflake_saml2_integration/resource.tf new file mode 100644 index 0000000000..66f435fa57 --- /dev/null +++ b/examples/resources/snowflake_saml2_integration/resource.tf @@ -0,0 +1,30 @@ +# basic resource +# each pem file contains a base64 encoded IdP signing certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers. +resource "snowflake_saml2_integration" "saml_integration" { + name = "saml_integration" + saml2_provider = "CUSTOM" + saml2_issuer = "test_issuer" + saml2_sso_url = "https://example.com" + saml2_x509_cert = file("cert.pem") +} +# resource with all fields set +resource "snowflake_saml2_integration" "test" { + allowed_email_patterns = ["^(.+dev)@example.com$"] + allowed_user_domains = ["example.com"] + comment = "foo" + enabled = true + name = "saml_integration" + saml2_enable_sp_initiated = true + saml2_force_authn = true + saml2_issuer = "foo" + saml2_post_logout_redirect_url = "https://example.com" + saml2_provider = "CUSTOM" + saml2_requested_nameid_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + saml2_sign_request = true + saml2_snowflake_acs_url = "example.snowflakecomputing.com/fed/login" + saml2_snowflake_issuer_url = "example.snowflakecomputing.com/fed/login" + saml2_snowflake_x509_cert = file("snowflake_cert.pem") + saml2_sp_initiated_login_page_label = "foo" + saml2_sso_url = "https://example.com" + saml2_x509_cert = file("cert.pem") +} diff --git a/examples/resources/snowflake_scim_integration/import.sh b/examples/resources/snowflake_scim_integration/import.sh index 86e162e445..365c14b973 100644 --- a/examples/resources/snowflake_scim_integration/import.sh +++ b/examples/resources/snowflake_scim_integration/import.sh @@ -1 +1 @@ -terraform import snowflake_scim_integration.example name +terraform import snowflake_scim_integration.example "name" diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index f04be3e850..9e2d11420a 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -139,6 +139,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.RowAccessPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.RowAccessPolicies.ShowByID) }, + resources.Saml2SecurityIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.SecurityIntegrations.ShowByID) + }, resources.Schema: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Schemas.ShowByID) }, diff --git a/pkg/acceptance/helpers/common.go b/pkg/acceptance/helpers/common.go index 4c4aa95fb6..153b007e39 100644 --- a/pkg/acceptance/helpers/common.go +++ b/pkg/acceptance/helpers/common.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "log" - "regexp" - "strings" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" @@ -62,15 +60,6 @@ func hasGranteeName(grants []sdk.Grant, role sdk.AccountObjectIdentifier) bool { return false } -// MatchAllStringsInOrderNonOverlapping returns a regex matching every string in parts. Matchings are non overlapping. -func MatchAllStringsInOrderNonOverlapping(parts []string) *regexp.Regexp { - escapedParts := make([]string, len(parts)) - for i := range parts { - escapedParts[i] = regexp.QuoteMeta(parts[i]) - } - return regexp.MustCompile(strings.Join(escapedParts, "((.|\n)*)")) -} - // AssertErrorContainsPartsFunc returns a function asserting error message contains each string in parts func AssertErrorContainsPartsFunc(t *testing.T, parts []string) resource.ErrorCheckFunc { t.Helper() diff --git a/pkg/acceptance/helpers/common_test.go b/pkg/acceptance/helpers/common_test.go deleted file mode 100644 index ccad8dcb51..0000000000 --- a/pkg/acceptance/helpers/common_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package helpers - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMatchAllStringsInOrderNonOverlapping(t *testing.T) { - testCases := map[string]struct { - parts []string - text string - wantMatch bool - }{ - "empty parts and text": { - parts: []string{}, - text: "", - wantMatch: true, - }, - "empty parts": { - parts: []string{}, - text: "xyz", - wantMatch: true, - }, - "empty text": { - parts: []string{"a", "b"}, - text: "", - }, - "matching non empty": { - parts: []string{"a", "b"}, - text: "xyaxyb", - wantMatch: true, - }, - "partial matching": { - parts: []string{"a", "b"}, - text: "axyz", - }, - "not matching": { - parts: []string{"a", "b"}, - text: "xyz", - }, - "wrong order": { - parts: []string{"a", "b"}, - text: "ba", - }, - "overlapping match": { - parts: []string{"abb", "bba"}, - text: "abba", - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - regex := MatchAllStringsInOrderNonOverlapping(tc.parts) - require.Equal(t, tc.wantMatch, regex.Match([]byte(tc.text))) - }) - } -} diff --git a/pkg/acceptance/helpers/random/certs.go b/pkg/acceptance/helpers/random/certs.go index 80a53a83eb..c4bc17460d 100644 --- a/pkg/acceptance/helpers/random/certs.go +++ b/pkg/acceptance/helpers/random/certs.go @@ -4,8 +4,10 @@ import ( "bytes" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" "encoding/pem" "fmt" "math/big" @@ -38,8 +40,8 @@ func GenerateX509(t *testing.T) string { return encode(t, "CERTIFICATE", caBytes) } -// GenerateRSA returns an RSA public key without BEGIN and END markers. -func GenerateRSAPublicKey(t *testing.T) string { +// GenerateRSA returns an RSA public key without BEGIN and END markers, and key's hash. +func GenerateRSAPublicKey(t *testing.T) (string, string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) @@ -47,7 +49,13 @@ func GenerateRSAPublicKey(t *testing.T) string { pub := key.Public() b, err := x509.MarshalPKIXPublicKey(pub.(*rsa.PublicKey)) require.NoError(t, err) - return encode(t, "RSA PUBLIC KEY", b) + return encode(t, "RSA PUBLIC KEY", b), hash(t, b) +} + +func hash(t *testing.T, b []byte) string { + t.Helper() + hash := sha256.Sum256(b) + return base64.StdEncoding.EncodeToString(hash[:]) } func encode(t *testing.T, pemType string, b []byte) string { diff --git a/pkg/acceptance/helpers/security_integration_client.go b/pkg/acceptance/helpers/security_integration_client.go index 08d854fbb4..89954b4b7b 100644 --- a/pkg/acceptance/helpers/security_integration_client.go +++ b/pkg/acceptance/helpers/security_integration_client.go @@ -27,7 +27,7 @@ func (c *SecurityIntegrationClient) client() sdk.SecurityIntegrations { func (c *SecurityIntegrationClient) CreateSaml2(t *testing.T, id sdk.AccountObjectIdentifier) (*sdk.SecurityIntegration, func()) { t.Helper() - return c.CreateSaml2WithRequest(t, sdk.NewCreateSaml2SecurityIntegrationRequest(id, false, c.ids.Alpha(), "https://example.com", "Custom", random.GenerateX509(t))) + return c.CreateSaml2WithRequest(t, sdk.NewCreateSaml2SecurityIntegrationRequest(id, c.ids.Alpha(), "https://example.com", "Custom", random.GenerateX509(t))) } func (c *SecurityIntegrationClient) CreateSaml2WithRequest(t *testing.T, request *sdk.CreateSaml2SecurityIntegrationRequest) (*sdk.SecurityIntegration, func()) { @@ -48,6 +48,19 @@ func (c *SecurityIntegrationClient) CreateScim(t *testing.T) (*sdk.SecurityInteg return c.CreateScimWithRequest(t, sdk.NewCreateScimSecurityIntegrationRequest(c.ids.RandomAccountObjectIdentifier(), sdk.ScimSecurityIntegrationScimClientGeneric, sdk.ScimSecurityIntegrationRunAsRoleGenericScimProvisioner)) } +func (c *SecurityIntegrationClient) UpdateSaml2(t *testing.T, request *sdk.AlterSaml2SecurityIntegrationRequest) { + t.Helper() + ctx := context.Background() + + err := c.client().AlterSaml2(ctx, request) + require.NoError(t, err) +} + +func (c *SecurityIntegrationClient) UpdateSaml2ForceAuthn(t *testing.T, id sdk.AccountObjectIdentifier, forceAuthn bool) { + t.Helper() + c.UpdateSaml2(t, sdk.NewAlterSaml2SecurityIntegrationRequest(id).WithSet(*sdk.NewSaml2IntegrationSetRequest().WithSaml2ForceAuthn(forceAuthn))) +} + func (c *SecurityIntegrationClient) CreateScimWithRequest(t *testing.T, request *sdk.CreateScimSecurityIntegrationRequest) (*sdk.SecurityIntegration, func()) { t.Helper() ctx := context.Background() diff --git a/pkg/acceptance/importchecks/import_checks.go b/pkg/acceptance/importchecks/import_checks.go index 68b3721485..fe80b244cd 100644 --- a/pkg/acceptance/importchecks/import_checks.go +++ b/pkg/acceptance/importchecks/import_checks.go @@ -1,6 +1,7 @@ package importchecks import ( + "errors" "fmt" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -19,6 +20,22 @@ func ComposeImportStateCheck(fs ...resource.ImportStateCheckFunc) resource.Impor } } +// ComposeAggregateImportStateCheck does the same as ComposeImportStateCheck, but it aggregates all the occurred errors, +// instead of returning the first encountered one. +func ComposeAggregateImportStateCheck(fs ...resource.ImportStateCheckFunc) resource.ImportStateCheckFunc { + return func(s []*terraform.InstanceState) error { + var result []error + + for i, f := range fs { + if err := f(s); err != nil { + result = append(result, fmt.Errorf("check %d/%d error: %w", i+1, len(fs), err)) + } + } + + return errors.Join(result...) + } +} + // TestCheckResourceAttrInstanceState is based on unexported testCheckResourceAttrInstanceState from teststep_providers_test.go func TestCheckResourceAttrInstanceState(id string, attributeName, attributeValue string) resource.ImportStateCheckFunc { return func(is []*terraform.InstanceState) error { @@ -39,3 +56,39 @@ func TestCheckResourceAttrInstanceState(id string, attributeName, attributeValue return fmt.Errorf("attribute %s not found in instance state", attributeName) } } + +// TestCheckResourceAttrNotInInstanceState is based on unexported testCheckResourceAttrInstanceState from teststep_providers_test.go, +// but instead of comparing values, it only checks if the attribute is present in the InstanceState. +func TestCheckResourceAttrNotInInstanceState(id string, attributeName string) resource.ImportStateCheckFunc { + return func(is []*terraform.InstanceState) error { + for _, v := range is { + if v.ID != id { + continue + } + + if _, ok := v.Attributes[attributeName]; ok { + return fmt.Errorf("attribute %s found in instance state, but expected not to be there", attributeName) + } + } + + return nil + } +} + +// TestCheckResourceAttrInstanceStateSet is based on unexported testCheckResourceAttrInstanceState from teststep_providers_test.go, +// but instead of comparing values, it only checks if the value is set. +func TestCheckResourceAttrInstanceStateSet(id string, attributeName string) resource.ImportStateCheckFunc { + return func(is []*terraform.InstanceState) error { + for _, v := range is { + if v.ID != id { + continue + } + + if _, ok := v.Attributes[attributeName]; ok { + return nil + } + } + + return fmt.Errorf("attribute %s not found in instance state", attributeName) + } +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index c3b04d55d9..8c98fdd90a 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -461,6 +461,7 @@ func getResources() map[string]*schema.Resource { "snowflake_role": resources.Role(), "snowflake_row_access_policy": resources.RowAccessPolicy(), "snowflake_saml_integration": resources.SAMLIntegration(), + "snowflake_saml2_integration": resources.SAML2Integration(), "snowflake_schema": resources.Schema(), "snowflake_scim_integration": resources.SCIMIntegration(), "snowflake_secondary_database": resources.SecondaryDatabase(), diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index daef998c37..6b9fd7add1 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -29,6 +29,7 @@ const ( ResourceMonitor resource = "snowflake_resource_monitor" Role resource = "snowflake_role" RowAccessPolicy resource = "snowflake_row_access_policy" + Saml2SecurityIntegration resource = "snowflake_saml2_integration" Schema resource = "snowflake_schema" ScimSecurityIntegration resource = "snowflake_scim_integration" SecondaryDatabase resource = "snowflake_secondary_database" diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index 72183af3e5..060c43df5c 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -58,6 +58,30 @@ func ParameterValueComputedIf(key string, parameters []*sdk.Parameter, objectPar } } +// ForceNewIfChangeToEmptySlice sets a ForceNew for a list field which was set to an empty value. +func ForceNewIfChangeToEmptySlice[T any](key string) schema.CustomizeDiffFunc { + return customdiff.ForceNewIfChange(key, func(ctx context.Context, oldValue, newValue, meta any) bool { + oldList, newList := oldValue.([]T), newValue.([]T) + return len(oldList) > 0 && len(newList) == 0 + }) +} + +// ForceNewIfChangeToEmptySet sets a ForceNew for a list field which was set to an empty value. +func ForceNewIfChangeToEmptySet(key string) schema.CustomizeDiffFunc { + return customdiff.ForceNewIfChange(key, func(ctx context.Context, oldValue, newValue, meta any) bool { + oldList, newList := oldValue.(*schema.Set).List(), newValue.(*schema.Set).List() + return len(oldList) > 0 && len(newList) == 0 + }) +} + +// ForceNewIfChangeToEmptyString sets a ForceNew for a string field which was set to an empty value. +func ForceNewIfChangeToEmptyString(key string) schema.CustomizeDiffFunc { + return customdiff.ForceNewIfChange(key, func(ctx context.Context, oldValue, newValue, meta any) bool { + oldString, newString := oldValue.(string), newValue.(string) + return len(oldString) > 0 && len(newString) == 0 + }) +} + // TODO [follow-up PR]: test func ComputedIfAnyAttributeChanged(key string, changedAttributeKeys ...string) schema.CustomizeDiffFunc { return customdiff.ComputedIf(key, func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) bool { diff --git a/pkg/resources/custom_diffs_test.go b/pkg/resources/custom_diffs_test.go index 17bcc3ae90..6e791672eb 100644 --- a/pkg/resources/custom_diffs_test.go +++ b/pkg/resources/custom_diffs_test.go @@ -5,7 +5,6 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -31,7 +30,11 @@ func TestParameterValueComputedIf(t *testing.T) { sdk.AccountParameterLogLevel, func(v any) string { return v.(string) }, ) - return createProviderWithValuePropertyAndCustomDiff(t, schema.TypeString, customDiff) + return createProviderWithValuePropertyAndCustomDiff(t, &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, customDiff) } t.Run("config: true - state: true - level: different - value: same", func(t *testing.T) { @@ -110,17 +113,13 @@ func TestParameterValueComputedIf(t *testing.T) { // of getting into this situation would be in create operation for which custom diffs are skipped. } -func createProviderWithValuePropertyAndCustomDiff(t *testing.T, valueType schema.ValueType, customDiffFunc schema.CustomizeDiffFunc) *schema.Provider { +func createProviderWithValuePropertyAndCustomDiff(t *testing.T, valueSchema *schema.Schema, customDiffFunc schema.CustomizeDiffFunc) *schema.Provider { t.Helper() return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ "test": { Schema: map[string]*schema.Schema{ - "value": { - Type: valueType, - Computed: true, - Optional: true, - }, + "value": valueSchema, }, CustomizeDiff: customDiffFunc, }, @@ -143,3 +142,202 @@ func calculateDiff(t *testing.T, providerConfig *schema.Provider, rawConfigValue require.NoError(t, err) return diff } + +func calculateDiffFromAttributes(t *testing.T, providerConfig *schema.Provider, oldValue map[string]string, newValue map[string]any) *terraform.InstanceDiff { + t.Helper() + diff, err := providerConfig.ResourcesMap["test"].Diff( + context.Background(), + &terraform.InstanceState{ + Attributes: oldValue, + }, + &terraform.ResourceConfig{ + Config: newValue, + }, + &provider.Context{Client: acc.Client(t)}, + ) + require.NoError(t, err) + return diff +} + +func TestForceNewIfChangeToEmptyString(t *testing.T) { + tests := []struct { + name string + stateValue map[string]string + rawConfigValue map[string]any + wantForceNew bool + }{ + { + name: "empty to non-empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{ + "value": "foo", + }, + wantForceNew: false, + }, { + name: "empty to empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{}, + wantForceNew: false, + }, { + name: "non-empty to empty", + stateValue: map[string]string{ + "value": "foo", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, { + name: "non-empty to non-empty", + stateValue: map[string]string{ + "value": "bar", + }, + rawConfigValue: map[string]any{ + "value": "foo", + }, + wantForceNew: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + customDiff := resources.ForceNewIfChangeToEmptyString( + "value", + ) + provider := createProviderWithValuePropertyAndCustomDiff(t, &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, customDiff) + diff := calculateDiffFromAttributes( + t, + provider, + tt.stateValue, + tt.rawConfigValue, + ) + assert.Equal(t, tt.wantForceNew, diff.RequiresNew()) + }) + } +} + +func TestForceNewIfChangeToEmptySlice(t *testing.T) { + tests := []struct { + name string + stateValue map[string]string + rawConfigValue map[string]any + wantForceNew bool + }{ + { + name: "empty to non-empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{ + "value": []any{"foo"}, + }, + wantForceNew: false, + }, { + name: "empty to empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{}, + wantForceNew: false, + }, { + name: "non-empty to empty", + stateValue: map[string]string{ + "value.#": "1", + "value.0": "foo", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, { + name: "non-empty to non-empty", + stateValue: map[string]string{ + "value.#": "2", + "value.0": "foo", + "value.1": "bar", + }, + rawConfigValue: map[string]any{ + "value": []any{"foo"}, + }, + wantForceNew: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + customDiff := resources.ForceNewIfChangeToEmptySlice[any]( + "value", + ) + provider := createProviderWithValuePropertyAndCustomDiff(t, &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, customDiff) + diff := calculateDiffFromAttributes( + t, + provider, + tt.stateValue, + tt.rawConfigValue, + ) + assert.Equal(t, tt.wantForceNew, diff.RequiresNew()) + }) + } +} + +func TestForceNewIfChangeToEmptySet(t *testing.T) { + tests := []struct { + name string + stateValue map[string]string + rawConfigValue map[string]any + wantForceNew bool + }{ + { + name: "empty to non-empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{ + "value": []any{"foo"}, + }, + wantForceNew: false, + }, { + name: "empty to empty", + stateValue: map[string]string{}, + rawConfigValue: map[string]any{}, + wantForceNew: false, + }, { + name: "non-empty to empty", + stateValue: map[string]string{ + "value.#": "1", + "value.2577344683": "CREATE DATABASE", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, { + name: "non-empty to non-empty", + stateValue: map[string]string{ + "value.#": "2", + "value.0": "foo", + "value.1": "bar", + }, + rawConfigValue: map[string]any{ + "value": []any{"foo"}, + }, + wantForceNew: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diff := calculateDiffFromAttributes(t, + createProviderWithValuePropertyAndCustomDiff(t, + &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + resources.ForceNewIfChangeToEmptySet( + "value", + ), + ), + tt.stateValue, + tt.rawConfigValue, + ) + assert.Equal(t, tt.wantForceNew, diff.RequiresNew()) + }) + } +} diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go index 1b4170d41b..6447fa8e93 100644 --- a/pkg/resources/helpers_test.go +++ b/pkg/resources/helpers_test.go @@ -191,136 +191,6 @@ func queriedPrivilegesContainAtLeast(query func(client *sdk.Client, ctx context. } } -func TestGetFirstNestedObjectByKey(t *testing.T) { - d := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ - "int_property": { - Type: schema.TypeList, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeInt, - }, - }, - }, - }, - "string_property": { - Type: schema.TypeList, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeString, - }, - }, - }, - }, - "list_property": { - Type: schema.TypeList, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - "multiple_list_properties": { - Type: schema.TypeList, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - "list": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "empty list": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "not_property": { - Type: schema.TypeString, - }, - }, map[string]any{ - "int_property": []any{ - map[string]any{ - "value": 123, - }, - }, - "string_property": []any{ - map[string]any{ - "value": "some string", - }, - }, - "list": []any{"one"}, - "empty_list": []any{}, - "list_property": []any{ - map[string]any{ - "value": []any{"one", "two", "three"}, - }, - }, - "multiple_list_properties": []any{ - map[string]any{ - "value": []any{"one", "two", "three"}, - }, - map[string]any{ - "value": []any{"one", "two", "three"}, - }, - }, - "not_property": "not a property", - }) - - intValue, err := resources.GetPropertyOfFirstNestedObjectByKey[int](d, "int_property", "value") - assert.NoError(t, err) - assert.Equal(t, 123, *intValue) - - stringValue, err := resources.GetPropertyOfFirstNestedObjectByKey[string](d, "string_property", "value") - assert.NoError(t, err) - assert.Equal(t, "some string", *stringValue) - - listValue, err := resources.GetPropertyOfFirstNestedObjectByKey[[]any](d, "list_property", "value") - assert.NoError(t, err) - assert.Equal(t, []any{"one", "two", "three"}, *listValue) - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "non_existing_property_key", "non_existing_value_key") - assert.ErrorContains(t, err, "nested property non_existing_property_key not found") - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "not_property", "value") - assert.ErrorContains(t, err, "nested property not_property is not an array or has incorrect number of values: 0, expected: 1") - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "empty_list", "value") - assert.ErrorContains(t, err, "nested property empty_list not found") // Empty list is a default value, so it's treated as "not set" - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "multiple_list_properties", "value") - assert.ErrorContains(t, err, "nested property multiple_list_properties is not an array or has incorrect number of values: 2, expected: 1") - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "list", "value") - assert.ErrorContains(t, err, "nested property list is not of type map[string]any, got: string") - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[any](d, "int_property", "non_existing_value_key") - assert.ErrorContains(t, err, "nested value key non_existing_value_key couldn't be found in the nested property map int_property") - - _, err = resources.GetPropertyOfFirstNestedObjectByKey[int](d, "string_property", "value") - assert.ErrorContains(t, err, "nested property string_property.value is not of type int, got: string") -} - func TestListDiff(t *testing.T) { testCases := []struct { Name string diff --git a/pkg/resources/saml2_integration.go b/pkg/resources/saml2_integration.go new file mode 100644 index 0000000000..259c8847df --- /dev/null +++ b/pkg/resources/saml2_integration.go @@ -0,0 +1,849 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var saml2IntegrationSchema = 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.TypeString, + Optional: true, + Default: "unknown", + ValidateDiagFunc: StringInSlice([]string{"true", "false"}, false), + DiffSuppressFunc: SuppressIfAny(ignoreCaseSuppressFunc, IgnoreChangeToCurrentSnowflakeValueInShow("enabled")), + Description: "Specifies whether this security integration is enabled or disabled. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.", + }, + "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, + ValidateDiagFunc: sdkValidation(sdk.ToSaml2SecurityIntegrationSaml2ProviderOption), + Description: fmt.Sprintf("The string describing the IdP. Valid options are: %v.", sdk.AllSaml2SecurityIntegrationSaml2Providers), + }, + "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, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_sp_initiated_login_page_label"), + Description: "The string containing the label to display after the Log In With button on the login page. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations.", + }, + "saml2_enable_sp_initiated": { + Type: schema.TypeString, + Optional: true, + Default: "unknown", + ValidateDiagFunc: StringInSlice([]string{"true", "false"}, false), + DiffSuppressFunc: SuppressIfAny(ignoreCaseSuppressFunc, IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_enable_sp_initiated")), + 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. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.", + }, + "saml2_sign_request": { + Type: schema.TypeString, + Optional: true, + Default: "unknown", + ValidateDiagFunc: StringInSlice([]string{"true", "false"}, false), + DiffSuppressFunc: SuppressIfAny(ignoreCaseSuppressFunc, IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_sign_request")), + 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. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.", + }, + "saml2_requested_nameid_format": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: sdkValidation(sdk.ToSaml2SecurityIntegrationSaml2RequestedNameidFormatOption), + DiffSuppressFunc: SuppressIfAny(ignoreCaseSuppressFunc, IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_requested_nameid_format")), + Description: fmt.Sprintf("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. Valid options are: %v", sdk.AllSaml2SecurityIntegrationSaml2RequestedNameidFormats), + }, + "saml2_post_logout_redirect_url": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_post_logout_redirect_url"), + 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.TypeString, + Optional: true, + Default: "unknown", + ValidateDiagFunc: StringInSlice([]string{"true", "false"}, false), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_force_authn"), + 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. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.", + }, + "saml2_snowflake_issuer_url": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_snowflake_issuer_url"), + 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.", + }, + "saml2_snowflake_acs_url": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("saml2_snowflake_acs_url"), + 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.", + }, + "allowed_user_domains": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "A list of email domains that can authenticate with a SAML2 security integration. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations.", + }, + "allowed_email_patterns": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "A list of regular expressions that email addresses are matched against to authenticate with a SAML2 security integration. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the integration.", + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW SECURITY INTEGRATION` for the given integration.", + Elem: &schema.Resource{ + Schema: schemas.ShowSecurityIntegrationSchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE SECURITY INTEGRATION` for the given integration.", + Elem: &schema.Resource{ + Schema: schemas.DescribeSaml2IntegrationSchema, + }, + }, +} + +func SAML2Integration() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSAML2Integration, + ReadContext: ReadContextSAML2Integration(true), + UpdateContext: UpdateContextSAML2Integration, + DeleteContext: DeleteContextSAM2LIntegration, + + Schema: saml2IntegrationSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportSaml2Integration, + }, + + CustomizeDiff: customdiff.All( + ForceNewIfChangeToEmptySet("allowed_user_domains"), + ForceNewIfChangeToEmptySet("allowed_email_patterns"), + ForceNewIfChangeToEmptyString("saml2_snowflake_issuer_url"), + ForceNewIfChangeToEmptyString("saml2_snowflake_acs_url"), + ForceNewIfChangeToEmptyString("saml2_sp_initiated_login_page_label"), + ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "name", "enabled", "comment"), + ComputedIfAnyAttributeChanged(DescribeOutputAttributeName, "saml2_issuer", "saml2_sso_url", "saml2_provider", "saml2_x509_cert", + "saml2_sp_initiated_login_page_label", "saml2_enable_sp_initiated", "saml2_sign_request", "saml2_requtedted_nameid_format", + "saml2_post_logout_redirect_url", "saml2_force_authn", "saml2_snowflake_issuer_url", "saml2_snowflake_acs_url", "allowed_user_domains", + "allowed_email_patterns"), + ), + } +} + +func ImportSaml2Integration(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting saml2 integration import") + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + integration, err := client.SecurityIntegrations.ShowByID(ctx, id) + if err != nil { + return nil, err + } + + integrationProperties, err := client.SecurityIntegrations.Describe(ctx, id) + if err != nil { + return nil, err + } + + if err := d.Set("name", sdk.NewAccountObjectIdentifier(integration.Name).Name()); err != nil { + return nil, err + } + if err := d.Set("comment", integration.Comment); err != nil { + return nil, err + } + if err := d.Set("enabled", fmt.Sprintf("%t", integration.Enabled)); err != nil { + return nil, err + } + + samlIssuer, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_ISSUER" }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 saml issuer, err = %w", err) + } + if err := d.Set("saml2_issuer", samlIssuer.Value); err != nil { + return nil, err + } + + ssoUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_SSO_URL" }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 sso url, err = %w", err) + } + if err := d.Set("saml2_sso_url", ssoUrl.Value); err != nil { + return nil, err + } + + samlProvider, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_PROVIDER" }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 provider, err = %w", err) + } + if err := d.Set("saml2_provider", samlProvider.Value); err != nil { + return nil, err + } + + x509Cert, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_X509_CERT" }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 x509 cert, err = %w", err) + } + if err := d.Set("saml2_x509_cert", x509Cert.Value); err != nil { + return nil, err + } + + spInitiatedLoginPageLabel, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SP_INITIATED_LOGIN_PAGE_LABEL" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 sp initiated login page label, err = %w", err) + } + if err := d.Set("saml2_sp_initiated_login_page_label", spInitiatedLoginPageLabel.Value); err != nil { + return nil, err + } + + enableSpInitiated, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_ENABLE_SP_INITIATED" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 enable sp initiated, err = %w", err) + } + if err := d.Set("saml2_enable_sp_initiated", enableSpInitiated.Value); err != nil { + return nil, err + } + + signRequest, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SIGN_REQUEST" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 sign request, err = %w", err) + } + if err := d.Set("saml2_sign_request", signRequest.Value); err != nil { + return nil, err + } + + requestedNameIdFormat, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_REQUESTED_NAMEID_FORMAT" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 requested nameid format, err = %w", err) + } + if err := d.Set("saml2_requested_nameid_format", requestedNameIdFormat.Value); err != nil { + return nil, err + } + + postLogoutRedirectUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_POST_LOGOUT_REDIRECT_URL" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 post logout redirect url, err = %w", err) + } + if err := d.Set("saml2_post_logout_redirect_url", postLogoutRedirectUrl.Value); err != nil { + return nil, err + } + + forceAuthn, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_FORCE_AUTHN" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 force authn, err = %w", err) + } + if err := d.Set("saml2_force_authn", forceAuthn.Value); err != nil { + return nil, err + } + + snowflakeIssuerUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SNOWFLAKE_ISSUER_URL" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 snowflake issuer url, err = %w", err) + } + if err := d.Set("saml2_snowflake_issuer_url", snowflakeIssuerUrl.Value); err != nil { + return nil, err + } + + snowflakeAcsUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SNOWFLAKE_ACS_URL" + }) + if err != nil { + return nil, fmt.Errorf("failed to find saml2 snowflake acs url, err = %w", err) + } + if err := d.Set("saml2_snowflake_acs_url", snowflakeAcsUrl.Value); err != nil { + return nil, err + } + + allowedUserDomains, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "ALLOWED_USER_DOMAINS" + }) + if err != nil { + return nil, fmt.Errorf("failed to find allowed user domains, err = %w", err) + } + if err := d.Set("allowed_user_domains", sdk.ParseCommaSeparatedStringArray(allowedUserDomains.Value)); err != nil { + return nil, err + } + + allowedEmailDomains, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "ALLOWED_EMAIL_PATTERNS" + }) + if err != nil { + return nil, fmt.Errorf("failed to find allowed email patterns, err = %w", err) + } + if err := d.Set("allowed_email_patterns", sdk.ParseCommaSeparatedStringArray(allowedEmailDomains.Value)); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextSAML2Integration(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + samlProvider, err := sdk.ToSaml2SecurityIntegrationSaml2ProviderOption(d.Get("saml2_provider").(string)) + if err != nil { + return diag.FromErr(err) + } + + req := sdk.NewCreateSaml2SecurityIntegrationRequest( + sdk.NewAccountObjectIdentifier(d.Get("name").(string)), + d.Get("saml2_issuer").(string), + d.Get("saml2_sso_url").(string), + samlProvider, + d.Get("saml2_x509_cert").(string), + ) + + if v := d.Get("enabled").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithEnabled(parsed) + } + + if v, ok := d.GetOk("saml2_sp_initiated_login_page_label"); ok { + req.WithSaml2SpInitiatedLoginPageLabel(v.(string)) + } + + if v := d.Get("saml2_enable_sp_initiated").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithSaml2EnableSpInitiated(parsed) + } + + if v := d.Get("saml2_sign_request").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithSaml2SignRequest(parsed) + } + + if v, ok := d.GetOk("saml2_requested_nameid_format"); ok { + format, err := sdk.ToSaml2SecurityIntegrationSaml2RequestedNameidFormatOption(v.(string)) + if err != nil { + return diag.FromErr(err) + } + req.WithSaml2RequestedNameidFormat(format) + } + + if v, ok := d.GetOk("saml2_post_logout_redirect_url"); ok { + req.WithSaml2PostLogoutRedirectUrl(v.(string)) + } + + if v := d.Get("saml2_force_authn").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithSaml2ForceAuthn(parsed) + } + + if v, ok := d.GetOk("saml2_snowflake_issuer_url"); ok { + req.WithSaml2SnowflakeIssuerUrl(v.(string)) + } + + if v, ok := d.GetOk("saml2_snowflake_acs_url"); ok { + req.WithSaml2SnowflakeAcsUrl(v.(string)) + } + + if v, ok := d.GetOk("allowed_user_domains"); ok { + stringAllowedUserDomains := expandStringList(v.(*schema.Set).List()) + allowedUserDomains := make([]sdk.UserDomain, len(stringAllowedUserDomains)) + for i, v := range stringAllowedUserDomains { + allowedUserDomains[i] = sdk.UserDomain{ + Domain: v, + } + } + req.WithAllowedUserDomains(allowedUserDomains) + } + + if v, ok := d.GetOk("allowed_email_patterns"); ok { + stringAllowedEmailPatterns := expandStringList(v.(*schema.Set).List()) + allowedEmailPatterns := make([]sdk.EmailPattern, len(stringAllowedEmailPatterns)) + for i, v := range stringAllowedEmailPatterns { + allowedEmailPatterns[i] = sdk.EmailPattern{ + Pattern: v, + } + } + req.WithAllowedEmailPatterns(allowedEmailPatterns) + } + + if v, ok := d.GetOk("comment"); ok { + req.WithComment(v.(string)) + } + + if err := client.SecurityIntegrations.CreateSaml2(ctx, req); err != nil { + return diag.FromErr(err) + } + + d.SetId(d.Get("name").(string)) + + return ReadContextSAML2Integration(false)(ctx, d, meta) +} + +func ReadContextSAML2Integration(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + integration, err := client.SecurityIntegrations.ShowByID(ctx, id) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query security integration. Marking the resource as removed.", + Detail: fmt.Sprintf("Security integration name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } + + integrationProperties, err := client.SecurityIntegrations.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + if c := integration.Category; c != sdk.SecurityIntegrationCategory { + return diag.FromErr(fmt.Errorf("expected %v to be a %s integration, got %v", id, sdk.SecurityIntegrationCategory, c)) + } + + if err := d.Set("name", sdk.NewAccountObjectIdentifier(integration.Name).Name()); err != nil { + return diag.FromErr(err) + } + + samlIssuer, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_ISSUER" }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 saml issuer, err = %w", err)) + } + if err := d.Set("saml2_issuer", samlIssuer.Value); err != nil { + return diag.FromErr(err) + } + + ssoUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_SSO_URL" }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 sso url, err = %w", err)) + } + if err := d.Set("saml2_sso_url", ssoUrl.Value); err != nil { + return diag.FromErr(err) + } + + samlProvider, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_PROVIDER" }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 provider, err = %w", err)) + } + if err := d.Set("saml2_provider", samlProvider.Value); err != nil { + return diag.FromErr(err) + } + + x509Cert, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SAML2_X509_CERT" }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 x509 cert, err = %w", err)) + } + if err := d.Set("saml2_x509_cert", x509Cert.Value); err != nil { + return diag.FromErr(err) + } + + postLogoutRedirectUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_POST_LOGOUT_REDIRECT_URL" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 post logout redirect url, err = %w", err)) + } + if err := d.Set("saml2_post_logout_redirect_url", postLogoutRedirectUrl.Value); err != nil { + return diag.FromErr(err) + } + + allowedUserDomains, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "ALLOWED_USER_DOMAINS" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find allowed user domains, err = %w", err)) + } + if err := d.Set("allowed_user_domains", sdk.ParseCommaSeparatedStringArray(allowedUserDomains.Value)); err != nil { + return diag.FromErr(err) + } + + allowedEmailDomains, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "ALLOWED_EMAIL_PATTERNS" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find allowed email patterns, err = %w", err)) + } + if err := d.Set("allowed_email_patterns", sdk.ParseCommaSeparatedStringArray(allowedEmailDomains.Value)); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("comment", integration.Comment); err != nil { + return diag.FromErr(err) + } + + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"enabled", "enabled", integration.Enabled, integration.Enabled, nil}, + ); err != nil { + return diag.FromErr(err) + } + + enableSpInitiated, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_ENABLE_SP_INITIATED" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 enable sp initiated, err = %w", err)) + } + + signRequest, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SIGN_REQUEST" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 sign request, err = %w", err)) + } + + requestedNameIdFormat, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_REQUESTED_NAMEID_FORMAT" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 requested nameid format, err = %w", err)) + } + + forceAuthn, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_FORCE_AUTHN" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 force authn, err = %w", err)) + } + + snowflakeIssuerUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SNOWFLAKE_ISSUER_URL" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 snowflake issuer url, err = %w", err)) + } + + snowflakeAcsUrl, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SNOWFLAKE_ACS_URL" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 snowflake acs url, err = %w", err)) + } + + spInitiatedLoginPageLabel, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { + return property.Name == "SAML2_SP_INITIATED_LOGIN_PAGE_LABEL" + }) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to find saml2 sp initiated login page label, err = %w", err)) + } + + if err = handleExternalChangesToObjectInDescribe(d, + describeMapping{"saml2_enable_sp_initiated", "saml2_enable_sp_initiated", enableSpInitiated.Value, enableSpInitiated.Value, nil}, + describeMapping{"saml2_sign_request", "saml2_sign_request", signRequest.Value, signRequest.Value, nil}, + describeMapping{"saml2_requested_nameid_format", "saml2_requested_nameid_format", requestedNameIdFormat.Value, requestedNameIdFormat.Value, nil}, + describeMapping{"saml2_force_authn", "saml2_force_authn", forceAuthn.Value, forceAuthn.Value, nil}, + describeMapping{"saml2_snowflake_acs_url", "saml2_snowflake_acs_url", snowflakeAcsUrl.Value, snowflakeAcsUrl.Value, nil}, + describeMapping{"saml2_snowflake_issuer_url", "saml2_snowflake_issuer_url", snowflakeIssuerUrl.Value, snowflakeIssuerUrl.Value, nil}, + describeMapping{"saml2_sp_initiated_login_page_label", "saml2_sp_initiated_login_page_label", spInitiatedLoginPageLabel.Value, spInitiatedLoginPageLabel.Value, nil}, + ); err != nil { + return diag.FromErr(err) + } + } + + // These are all identity sets, needed for the case where: + // - previous config was empty (therefore Snowflake defaults had been used) + // - new config have the same values that are already in SF + if !d.GetRawConfig().IsNull() { + if v := d.GetRawConfig().AsValueMap()["enabled"]; !v.IsNull() { + if err = d.Set("enabled", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_enable_sp_initiated"]; !v.IsNull() { + if err = d.Set("saml2_enable_sp_initiated", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_sign_request"]; !v.IsNull() { + if err = d.Set("saml2_sign_request", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_requested_nameid_format"]; !v.IsNull() { + if err = d.Set("saml2_requested_nameid_format", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_force_authn"]; !v.IsNull() { + if err = d.Set("saml2_force_authn", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_snowflake_acs_url"]; !v.IsNull() { + if err = d.Set("saml2_snowflake_acs_url", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_snowflake_issuer_url"]; !v.IsNull() { + if err = d.Set("saml2_snowflake_issuer_url", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["saml2_sp_initiated_login_page_label"]; !v.IsNull() { + if err = d.Set("saml2_sp_initiated_login_page_label", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + } + + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.SecurityIntegrationToSchema(integration)}); err != nil { + return diag.FromErr(err) + } + + if err = d.Set(DescribeOutputAttributeName, []map[string]any{schemas.DescribeSaml2IntegrationToSchema(integrationProperties)}); err != nil { + return diag.FromErr(err) + } + + return nil + } +} + +func UpdateContextSAML2Integration(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + set, unset := sdk.NewSaml2IntegrationSetRequest(), sdk.NewSaml2IntegrationUnsetRequest() + + if d.HasChange("enabled") { + if v := d.Get("enabled").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + set.WithEnabled(parsed) + } else { + // TODO(SNOW-1515781): UNSET is not implemented + set.WithEnabled(false) + } + } + + if d.HasChange("saml2_issuer") { + set.WithSaml2Issuer(d.Get("saml2_issuer").(string)) + } + + if d.HasChange("saml2_sso_url") { + set.WithSaml2SsoUrl(d.Get("saml2_sso_url").(string)) + } + + if d.HasChange("saml2_provider") { + valueRaw := d.Get("saml2_provider").(string) + value, err := sdk.ToSaml2SecurityIntegrationSaml2ProviderOption(valueRaw) + if err != nil { + return diag.FromErr(err) + } + set.WithSaml2Provider(value) + } + + if d.HasChange("saml2_x509_cert") { + set.WithSaml2X509Cert(d.Get("saml2_x509_cert").(string)) + } + + if d.HasChange("saml2_sp_initiated_login_page_label") { + // TODO(SNOW-1515781): UNSET is not implemented and SET with empty value is invalid (conditional ForceNew on unset) + set.WithSaml2SpInitiatedLoginPageLabel(d.Get("saml2_sp_initiated_login_page_label").(string)) + } + + if d.HasChange("saml2_enable_sp_initiated") { + if v := d.Get("saml2_enable_sp_initiated").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + set.WithSaml2EnableSpInitiated(parsed) + } else { + // TODO(SNOW-1515781): UNSET is not implemented + set.WithSaml2EnableSpInitiated(false) + } + } + + if d.HasChange("saml2_sign_request") { + if v := d.Get("saml2_sign_request").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + set.WithSaml2SignRequest(parsed) + } else { + // TODO(SNOW-1515781): UNSET is not implemented + set.WithSaml2SignRequest(false) + } + } + + if d.HasChange("saml2_requested_nameid_format") { + if v := d.Get("saml2_requested_nameid_format").(string); len(v) > 0 { + value, err := sdk.ToSaml2SecurityIntegrationSaml2RequestedNameidFormatOption(v) + if err != nil { + return diag.FromErr(err) + } + set.WithSaml2RequestedNameidFormat(value) + } else { + unset.WithSaml2RequestedNameidFormat(true) + } + } + + if d.HasChange("saml2_post_logout_redirect_url") { + if v := d.Get("saml2_post_logout_redirect_url").(string); len(v) > 0 { + set.WithSaml2PostLogoutRedirectUrl(v) + } else { + unset.WithSaml2PostLogoutRedirectUrl(true) + } + } + + if d.HasChange("saml2_force_authn") { + if v := d.Get("saml2_force_authn").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + set.WithSaml2ForceAuthn(parsed) + } else { + // TODO(SNOW-1515781): UNSET is not implemented + set.WithSaml2ForceAuthn(false) + } + } + + if d.HasChange("saml2_snowflake_issuer_url") { + // TODO(SNOW-1515781): UNSET is not implemented and SET with empty value is invalid (conditional ForceNew on unset) + set.WithSaml2SnowflakeIssuerUrl(d.Get("saml2_snowflake_issuer_url").(string)) + } + + if d.HasChange("saml2_snowflake_acs_url") { + // TODO(SNOW-1515781): UNSET is not implemented and SET with empty value is invalid (conditional ForceNew on unset) + set.WithSaml2SnowflakeAcsUrl(d.Get("saml2_snowflake_acs_url").(string)) + } + + if d.HasChange("allowed_user_domains") { + // TODO(SNOW-1515781): UNSET is not implemented and SET with empty list is invalid (conditional ForceNew on non-empty to empty set) + v := d.Get("allowed_user_domains").(*schema.Set).List() + userDomains := make([]sdk.UserDomain, len(v)) + for i := range v { + userDomains[i] = sdk.UserDomain{ + Domain: v[i].(string), + } + } + set.WithAllowedUserDomains(userDomains) + } + + if d.HasChange("allowed_email_patterns") { + // TODO(SNOW-SNOW-1515781): UNSET is not implemented and SET with empty list is invalid (conditional ForceNew on non-empty to empty set) + v := d.Get("allowed_email_patterns").(*schema.Set).List() + emailPatterns := make([]sdk.EmailPattern, len(v)) + for i := range v { + emailPatterns[i] = sdk.EmailPattern{ + Pattern: v[i].(string), + } + } + set.WithAllowedEmailPatterns(emailPatterns) + } + + if d.HasChange("comment") { + if v := d.Get("comment").(string); len(v) > 0 { + set.WithComment(v) + } else { + unset.WithComment(true) + } + } + + if !reflect.DeepEqual(*set, sdk.Saml2IntegrationSetRequest{}) { + if err := client.SecurityIntegrations.AlterSaml2(ctx, sdk.NewAlterSaml2SecurityIntegrationRequest(id).WithSet(*set)); err != nil { + return diag.FromErr(err) + } + } + if !reflect.DeepEqual(*unset, sdk.Saml2IntegrationUnsetRequest{}) { + if err := client.SecurityIntegrations.AlterSaml2(ctx, sdk.NewAlterSaml2SecurityIntegrationRequest(id).WithUnset(*unset)); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSAML2Integration(false)(ctx, d, meta) +} + +func DeleteContextSAM2LIntegration(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + client := meta.(*provider.Context).Client + + err := client.SecurityIntegrations.Drop(ctx, sdk.NewDropSecurityIntegrationRequest(sdk.NewAccountObjectIdentifier(id.Name())).WithIfExists(true)) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting integration", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } + } + + d.SetId("") + return nil +} diff --git a/pkg/resources/saml2_integration_acceptance_test.go b/pkg/resources/saml2_integration_acceptance_test.go new file mode 100644 index 0000000000..76586044b1 --- /dev/null +++ b/pkg/resources/saml2_integration_acceptance_test.go @@ -0,0 +1,1048 @@ +package resources_test + +import ( + "fmt" + "maps" + "regexp" + "strings" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_Saml2Integration_basic(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + issuer, issuer2 := acc.TestClient().Ids.Alpha(), acc.TestClient().Ids.Alpha() + cert, cert2 := random.GenerateX509(t), random.GenerateX509(t) + validUrl, validUrl2 := "http://example.com", "http://example2.com" + acsURL := acc.TestClient().Context.ACSURL(t) + issuerURL := acc.TestClient().Context.IssuerURL(t) + + m := func(issuer, provider, ssoUrl, x509Cert string, complete bool, unset bool) map[string]config.Variable { + c := map[string]config.Variable{ + "name": config.StringVariable(id.Name()), + "saml2_issuer": config.StringVariable(issuer), + "saml2_provider": config.StringVariable(provider), + "saml2_sso_url": config.StringVariable(ssoUrl), + "saml2_x509_cert": config.StringVariable(x509Cert), + } + if complete { + c["enabled"] = config.BoolVariable(true) + c["comment"] = config.StringVariable("foo") + c["saml2_enable_sp_initiated"] = config.BoolVariable(true) + c["saml2_force_authn"] = config.BoolVariable(true) + c["saml2_post_logout_redirect_url"] = config.StringVariable(validUrl) + c["saml2_requested_nameid_format"] = config.StringVariable(string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)) + c["saml2_sign_request"] = config.BoolVariable(true) + // TODO(SNOW-1479617): set saml2_snowflake_x509_cert + c["saml2_snowflake_acs_url"] = config.StringVariable(acsURL) + c["saml2_snowflake_issuer_url"] = config.StringVariable(issuerURL) + c["saml2_sp_initiated_login_page_label"] = config.StringVariable("foo") + c["allowed_email_patterns"] = config.ListVariable(config.StringVariable("^(.+dev)@example.com$")) + c["allowed_user_domains"] = config.ListVariable(config.StringVariable("example.com")) + } + // When unsetting, we have to keep those to prevent conditional force new being triggered + if unset { + c["saml2_snowflake_acs_url"] = config.StringVariable(acsURL) + c["saml2_snowflake_issuer_url"] = config.StringVariable(issuerURL) + c["saml2_sp_initiated_login_page_label"] = config.StringVariable("foo") + c["allowed_email_patterns"] = config.ListVariable(config.StringVariable("^(.+dev)@example.com$")) + c["allowed_user_domains"] = config.ListVariable(config.StringVariable("example.com")) + } + return c + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Saml2SecurityIntegration), + Steps: []resource.TestStep{ + // create with empty optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/basic"), + ConfigVariables: m(issuer, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl, cert, false, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "enabled", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_issuer", issuer), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sso_url", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_x509_cert", cert), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "saml2_sp_initiated_login_page_label"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_enable_sp_initiated", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sign_request", "unknown"), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "saml2_requested_nameid_format"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_force_authn", "unknown"), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_issuer_url"), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_acs_url"), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains"), + resource.TestCheckNoResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "comment", ""), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_issuer.0.value", issuer), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sso_url.0.value", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_provider.0.value", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_x509_cert.0.value", cert), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sp_initiated_login_page_label.0.value", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_enable_sp_initiated.0.value", "false"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_x509_cert.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sign_request.0.value", "false"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_requested_nameid_format.0.value", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_post_logout_redirect_url.0.value", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_force_authn.0.value", "false"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_issuer_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_acs_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_metadata.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_digest_methods_used.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_signature_methods_used.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_user_domains.0.value", "[]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_email_patterns.0.value", "[]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.comment.0.value", ""), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.integration_type", "SAML2"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.enabled", "false"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.comment", ""), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "show_output.0.created_on"), + ), + }, + // import - without optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/basic"), + ConfigVariables: m(issuer, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl, cert, false, false), + ResourceName: "snowflake_saml2_integration.test", + ImportState: true, + ImportStateCheck: importchecks.ComposeAggregateImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "enabled", "false"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_issuer", issuer), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sso_url", validUrl), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_x509_cert", cert), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sp_initiated_login_page_label", ""), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_enable_sp_initiated", "false"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sign_request", "false"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_requested_nameid_format", ""), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_post_logout_redirect_url", ""), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_force_authn", "false"), + importchecks.TestCheckResourceAttrInstanceStateSet(id.Name(), "saml2_snowflake_issuer_url"), + importchecks.TestCheckResourceAttrInstanceStateSet(id.Name(), "saml2_snowflake_acs_url"), + importchecks.TestCheckResourceAttrNotInInstanceState(id.Name(), "allowed_user_domains"), + importchecks.TestCheckResourceAttrNotInInstanceState(id.Name(), "allowed_email_patterns"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "comment", ""), + ), + }, + // set optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/complete"), + ConfigVariables: m(issuer2, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl2, cert2, true, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "enabled", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_issuer", issuer2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sso_url", validUrl2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_x509_cert", cert2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sp_initiated_login_page_label", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_enable_sp_initiated", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sign_request", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_requested_nameid_format", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_force_authn", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_issuer_url", issuerURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_acs_url", acsURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.0", "example.com"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.0", "^(.+dev)@example.com$"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "comment", "foo"), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_issuer.0.value", issuer2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sso_url.0.value", validUrl2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_provider.0.value", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_x509_cert.0.value", cert2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sp_initiated_login_page_label.0.value", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_enable_sp_initiated.0.value", "true"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_x509_cert.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sign_request.0.value", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_requested_nameid_format.0.value", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_post_logout_redirect_url.0.value", "http://example.com"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_force_authn.0.value", "true"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_issuer_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_acs_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_metadata.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_digest_methods_used.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_signature_methods_used.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_user_domains.0.value", "[example.com]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_email_patterns.0.value", "[^(.+dev)@example.com$]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.comment.0.value", "foo"), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.integration_type", "SAML2"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.enabled", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.comment", "foo"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "show_output.0.created_on"), + ), + }, + // import - complete + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/complete"), + ConfigVariables: m(issuer2, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl2, cert2, true, false), + ResourceName: "snowflake_saml2_integration.test", + ImportState: true, + ImportStateCheck: importchecks.ComposeAggregateImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "enabled", "true"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_issuer", issuer2), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sso_url", validUrl2), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_x509_cert", cert2), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sp_initiated_login_page_label", "foo"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_enable_sp_initiated", "true"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_sign_request", "true"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_requested_nameid_format", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_post_logout_redirect_url", validUrl), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "saml2_force_authn", "true"), + importchecks.TestCheckResourceAttrInstanceStateSet(id.Name(), "saml2_snowflake_issuer_url"), + importchecks.TestCheckResourceAttrInstanceStateSet(id.Name(), "saml2_snowflake_acs_url"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "allowed_user_domains.#", "1"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "allowed_user_domains.0", "example.com"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "allowed_email_patterns.#", "1"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "allowed_email_patterns.0", "^(.+dev)@example.com$"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "comment", "foo"), + ), + }, + // change values externally + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/complete"), + ConfigVariables: m(issuer2, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl2, cert2, true, false), + PreConfig: func() { + acc.TestClient().SecurityIntegration.UpdateSaml2(t, sdk.NewAlterSaml2SecurityIntegrationRequest(id). + WithUnset(*sdk.NewSaml2IntegrationUnsetRequest(). + WithSaml2RequestedNameidFormat(true). + WithSaml2PostLogoutRedirectUrl(true). + WithSaml2ForceAuthn(true). + WithComment(true))) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + planchecks.ExpectDrift("snowflake_saml2_integration.test", "saml2_requested_nameid_format", sdk.String(string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), sdk.String(string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatEmailAddress))), + planchecks.ExpectDrift("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", sdk.String(validUrl), sdk.String("")), + planchecks.ExpectDrift("snowflake_saml2_integration.test", "saml2_force_authn", sdk.String("true"), sdk.String("false")), + planchecks.ExpectDrift("snowflake_saml2_integration.test", "comment", sdk.String("foo"), sdk.String("")), + + planchecks.ExpectChange("snowflake_saml2_integration.test", "saml2_requested_nameid_format", tfjson.ActionUpdate, sdk.String(string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatEmailAddress)), sdk.String(string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified))), + planchecks.ExpectChange("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", tfjson.ActionUpdate, sdk.String(""), sdk.String(validUrl)), + planchecks.ExpectChange("snowflake_saml2_integration.test", "saml2_force_authn", tfjson.ActionUpdate, sdk.String("false"), sdk.String("true")), + planchecks.ExpectChange("snowflake_saml2_integration.test", "comment", tfjson.ActionUpdate, sdk.String(""), sdk.String("foo")), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "enabled", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_issuer", issuer2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sso_url", validUrl2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_x509_cert", cert2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sp_initiated_login_page_label", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_enable_sp_initiated", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sign_request", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_requested_nameid_format", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_force_authn", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_issuer_url", issuerURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_acs_url", acsURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.0", "example.com"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.0", "^(.+dev)@example.com$"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "comment", "foo"), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_issuer.0.value", issuer2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sso_url.0.value", validUrl2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_provider.0.value", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_x509_cert.0.value", cert2), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sp_initiated_login_page_label.0.value", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_enable_sp_initiated.0.value", "true"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_x509_cert.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sign_request.0.value", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_requested_nameid_format.0.value", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatUnspecified)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_post_logout_redirect_url.0.value", "http://example.com"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_force_authn.0.value", "true"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_issuer_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_acs_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_metadata.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_digest_methods_used.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_signature_methods_used.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_user_domains.0.value", "[example.com]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_email_patterns.0.value", "[^(.+dev)@example.com$]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.comment.0.value", "foo"), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.integration_type", "SAML2"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.enabled", "true"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.comment", "foo"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "show_output.0.created_on"), + ), + }, + // unset + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Saml2Integration/recreates"), + ConfigVariables: m(issuer, string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom), validUrl, cert, false, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "enabled", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_issuer", issuer), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sso_url", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_provider", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_x509_cert", cert), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sp_initiated_login_page_label", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_enable_sp_initiated", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_sign_request", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_requested_nameid_format", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_post_logout_redirect_url", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_force_authn", "unknown"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_issuer_url", issuerURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "saml2_snowflake_acs_url", acsURL), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_user_domains.0", "example.com"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "allowed_email_patterns.0", "^(.+dev)@example.com$"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "comment", ""), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_issuer.0.value", issuer), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sso_url.0.value", validUrl), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_provider.0.value", string(sdk.Saml2SecurityIntegrationSaml2ProviderCustom)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_x509_cert.0.value", cert), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sp_initiated_login_page_label.0.value", "foo"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_enable_sp_initiated.0.value", "false"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_x509_cert.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_sign_request.0.value", "false"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_requested_nameid_format.0.value", string(sdk.Saml2SecurityIntegrationSaml2RequestedNameidFormatEmailAddress)), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_post_logout_redirect_url.0.value", ""), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.saml2_force_authn.0.value", "false"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_issuer_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_acs_url.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_snowflake_metadata.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_digest_methods_used.0.value"), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "describe_output.0.saml2_signature_methods_used.0.value"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_user_domains.0.value", "[example.com]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.allowed_email_patterns.0.value", "[^(.+dev)@example.com$]"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "describe_output.0.comment.0.value", ""), + + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.integration_type", "SAML2"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.enabled", "false"), + resource.TestCheckResourceAttr("snowflake_saml2_integration.test", "show_output.0.comment", ""), + resource.TestCheckResourceAttrSet("snowflake_saml2_integration.test", "show_output.0.created_on"), + ), + }, + }, + }) +} + +func saml2ConfigWithAuthn(name, issuer, provider, ssoUrl, x509Cert string, forceAuthn bool) string { + return fmt.Sprintf(` +resource "snowflake_saml2_integration" "test" { + name = "%s" + saml2_issuer = "%s" + saml2_provider = "%s" + saml2_sso_url = "%s" + saml2_x509_cert = <