diff --git a/docs/resources/account_authentication_policy_attachment.md b/docs/resources/account_authentication_policy_attachment.md new file mode 100644 index 0000000000..40a93a5ba1 --- /dev/null +++ b/docs/resources/account_authentication_policy_attachment.md @@ -0,0 +1,35 @@ +--- +page_title: "snowflake_account_authentication_policy_attachment Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias. +--- + +# snowflake_account_authentication_policy_attachment (Resource) + +Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias. + +## Example Usage + +```terraform +resource "snowflake_authentication_policy" "default" { + database = "prod" + schema = "security" + name = "default_policy" +} + +resource "snowflake_account_authentication_policy_attachment" "attachment" { + authentication_policy = snowflake_authentication_policy.default.fully_qualified_name +} +``` + + +## Schema + +### Required + +- `authentication_policy` (String) Qualified name (`"db"."schema"."policy_name"`) of the authentication policy to apply to the current account. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/authentication_policy.md b/docs/resources/authentication_policy.md new file mode 100644 index 0000000000..ec117e944c --- /dev/null +++ b/docs/resources/authentication_policy.md @@ -0,0 +1,35 @@ +--- +page_title: "snowflake_authentication_policy Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_authentication_policy (Resource) + + + + + + +## Schema + +### Required + +- `database` (String) The database in which to create the authentication policy. +- `name` (String) Specifies the identifier for the authentication policy. +- `schema` (String) The schema in which to create the authentication policy. + +### Optional + +- `authentication_methods` (Set of String) A list of authentication methods that are allowed during login. This parameter accepts one or more of the following values: ALL, SAML, PASSWORD, OAUTH, KEYPAIR. +- `client_types` (Set of String) A list of clients that can authenticate with Snowflake. If a client tries to connect, and the client is not one of the valid CLIENT_TYPES, then the login attempt fails. Allowed values are ALL, SNOWFLAKE_UI, DRIVERS, SNOWSQL. The CLIENT_TYPES property of an authentication policy is a best effort method to block user logins based on specific clients. It should not be used as the sole control to establish a security boundary. +- `comment` (String) Specifies a comment for the authentication policy. +- `mfa_authentication_methods` (Set of String) A list of authentication methods that enforce multi-factor authentication (MFA) during login. Authentication methods not listed in this parameter do not prompt for multi-factor authentication. Allowed values are SAML and PASSWORD. +- `mfa_enrollment` (String) Determines whether a user must enroll in multi-factor authentication. Allowed values are REQUIRED and OPTIONAL. When REQUIRED is specified, Enforces users to enroll in MFA. If this value is used, then the CLIENT_TYPES parameter must include SNOWFLAKE_UI, because Snowsight is the only place users can enroll in multi-factor authentication (MFA). +- `security_integrations` (Set of String) A list of security integrations the authentication policy is associated with. This parameter has no effect when SAML or OAUTH are not in the AUTHENTICATION_METHODS list. All values in the SECURITY_INTEGRATIONS list must be compatible with the values in the AUTHENTICATION_METHODS list. For example, if SECURITY_INTEGRATIONS contains a SAML security integration, and AUTHENTICATION_METHODS contains OAUTH, then you cannot create the authentication policy. To allow all security integrations use ALL as parameter. + +### Read-Only + +- `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). +- `id` (String) The ID of this resource. diff --git a/docs/resources/user_authentication_policy_attachment.md b/docs/resources/user_authentication_policy_attachment.md new file mode 100644 index 0000000000..c4fe77a998 --- /dev/null +++ b/docs/resources/user_authentication_policy_attachment.md @@ -0,0 +1,39 @@ +--- +page_title: "snowflake_user_authentication_policy_attachment Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Specifies the authentication policy to use for a certain user. +--- + +# snowflake_user_authentication_policy_attachment (Resource) + +Specifies the authentication policy to use for a certain user. + +## Example Usage + +```terraform +resource "snowflake_user" "user" { + name = "USER_NAME" +} +resource "snowflake_authentication_policy" "ap" { + database = "prod" + schema = "security" + name = "default_policy" +} + +resource "snowflake_user_authentication_policy_attachment" "apa" { + authentication_policy_name = snowflake_authentication_policy.ap.fully_qualified_name + user_name = snowflake_user.user.name +} +``` + +## Schema + +### Required + +- `authentication_policy_name` (String) Fully qualified name of the authentication policy +- `user_name` (String) User name of the user you want to attach the authentication policy to + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/snowflake_authentication_policy/import.sh b/examples/resources/snowflake_authentication_policy/import.sh new file mode 100644 index 0000000000..81ffd621e8 --- /dev/null +++ b/examples/resources/snowflake_authentication_policy/import.sh @@ -0,0 +1 @@ +terraform import snowflake_authentication_policy.example '""."".""' diff --git a/examples/resources/snowflake_authentication_policy/resource.tf b/examples/resources/snowflake_authentication_policy/resource.tf new file mode 100644 index 0000000000..98745f4fe3 --- /dev/null +++ b/examples/resources/snowflake_authentication_policy/resource.tf @@ -0,0 +1,19 @@ +## Minimal +resource "snowflake_authentication_policy" "basic" { + database = "database_name" + schema = "schema_name" + name = "network_policy_name" +} + +## Complete (with every optional set) +resource "snowflake_authentication_policy" "complete" { + database = "database_name" + schema = "schema_name" + name = "network_policy_name" + authentication_methods = ["ALL"] + mfa_authentication_methods = ["SAML", "PASSWORD"] + mfa_enrollment = "OPTIONAL" + client_types = ["ALL"] + security_integrations = ["ALL"] + comment = "My authentication policy." +} \ No newline at end of file diff --git a/examples/resources/snowflake_network_policy/resource.tf b/examples/resources/snowflake_network_policy/resource.tf index 17c0e70fcb..d6cfe4bb62 100644 --- a/examples/resources/snowflake_network_policy/resource.tf +++ b/examples/resources/snowflake_network_policy/resource.tf @@ -4,7 +4,7 @@ resource "snowflake_network_policy" "basic" { } ## Complete (with every optional set) -resource "snowflake_network_policy" "basic" { +resource "snowflake_network_policy" "complete" { name = "network_policy_name" allowed_network_rule_list = [""] blocked_network_rule_list = [""] diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index 85985f0188..56ef3143ab 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -99,6 +99,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.ApiIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.ApiIntegrations.ShowByID) }, + resources.AuthenticationPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.AuthenticationPolicies.ShowByID) + }, resources.CortexSearchService: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.CortexSearchServices.ShowByID) }, @@ -451,6 +454,30 @@ func CheckUserPasswordPolicyAttachmentDestroy(t *testing.T) func(*terraform.Stat } } +// CheckUserAuthenticationPolicyAttachmentDestroy is a custom checks that should be later incorporated into generic CheckDestroy +func CheckUserAuthenticationPolicyAttachmentDestroy(t *testing.T) func(*terraform.State) error { + t.Helper() + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "snowflake_user_authentication_policy_attachment" { + continue + } + policyReferences, err := TestClient().PolicyReferences.GetPolicyReferences(t, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["user_name"]), sdk.PolicyEntityDomainUser) + if err != nil { + if strings.Contains(err.Error(), "does not exist or not authorized") { + // Note: this can happen if the Policy Reference or the User has been deleted as well; in this case, ignore the error + continue + } + return err + } + if len(policyReferences) > 0 { + return fmt.Errorf("user authentication policy attachment %v still exists", policyReferences[0].PolicyName) + } + } + return nil + } +} + func TestAccCheckGrantApplicationRoleDestroy(s *terraform.State) error { client := TestAccProvider.Meta().(*provider.Context).Client for _, rs := range s.RootModule().Resources { diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 36fbc19943..2513ea8817 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -421,15 +421,17 @@ func Provider() *schema.Provider { func getResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "snowflake_account": resources.Account(), - "snowflake_account_role": resources.AccountRole(), - "snowflake_account_password_policy_attachment": resources.AccountPasswordPolicyAttachment(), - "snowflake_account_parameter": resources.AccountParameter(), - "snowflake_alert": resources.Alert(), + "snowflake_account": resources.Account(), + "snowflake_account_authentication_policy_attachment": resources.AccountAuthenticationPolicyAttachment(), + "snowflake_account_role": resources.AccountRole(), + "snowflake_account_password_policy_attachment": resources.AccountPasswordPolicyAttachment(), + "snowflake_account_parameter": resources.AccountParameter(), + "snowflake_alert": resources.Alert(), "snowflake_api_authentication_integration_with_authorization_code_grant": resources.ApiAuthenticationIntegrationWithAuthorizationCodeGrant(), "snowflake_api_authentication_integration_with_client_credentials": resources.ApiAuthenticationIntegrationWithClientCredentials(), "snowflake_api_authentication_integration_with_jwt_bearer": resources.ApiAuthenticationIntegrationWithJwtBearer(), "snowflake_api_integration": resources.APIIntegration(), + "snowflake_authentication_policy": resources.AuthenticationPolicy(), "snowflake_cortex_search_service": resources.CortexSearchService(), "snowflake_database_old": resources.DatabaseOld(), "snowflake_database": resources.Database(), @@ -497,6 +499,7 @@ func getResources() map[string]*schema.Resource { "snowflake_task": resources.Task(), "snowflake_unsafe_execute": resources.UnsafeExecute(), "snowflake_user": resources.User(), + "snowflake_user_authentication_policy_attachment": resources.UserAuthenticationPolicyAttachment(), "snowflake_user_password_policy_attachment": resources.UserPasswordPolicyAttachment(), "snowflake_user_public_keys": resources.UserPublicKeys(), "snowflake_view": resources.View(), diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index 1de4d34f7f..f2521bb2f1 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -10,6 +10,7 @@ const ( ApiAuthenticationIntegrationWithClientCredentials resource = "snowflake_api_authentication_integration_with_client_credentials" ApiAuthenticationIntegrationWithJwtBearer resource = "snowflake_api_authentication_integration_with_jwt_bearer" ApiIntegration resource = "snowflake_api_integration" + AuthenticationPolicy resource = "snowflake_authentication_policy" CortexSearchService resource = "snowflake_cortex_search_service" DatabaseOld resource = "snowflake_database_old" Database resource = "snowflake_database" diff --git a/pkg/resources/account_authentication_policy_attachment.go b/pkg/resources/account_authentication_policy_attachment.go new file mode 100644 index 0000000000..628cee492c --- /dev/null +++ b/pkg/resources/account_authentication_policy_attachment.go @@ -0,0 +1,88 @@ +package resources + +import ( + "context" + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var accountAuthenticationPolicyAttachmentSchema = map[string]*schema.Schema{ + "authentication_policy": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Qualified name (`\"db\".\"schema\".\"policy_name\"`) of the authentication policy to apply to the current account.", + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + }, +} + +// AccountAuthenticationPolicyAttachment returns a pointer to the resource representing an account authentication policy attachment. +func AccountAuthenticationPolicyAttachment() *schema.Resource { + return &schema.Resource{ + Description: "Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias.", + + Create: CreateAccountAuthenticationPolicyAttachment, + Read: ReadAccountAuthenticationPolicyAttachment, + Delete: DeleteAccountAuthenticationPolicyAttachment, + + Schema: accountAuthenticationPolicyAttachmentSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +// CreateAccountAuthenticationPolicyAttachment implements schema.CreateFunc. +func CreateAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error { + client := meta.(*provider.Context).Client + ctx := context.Background() + + authenticationPolicy, ok := sdk.NewObjectIdentifierFromFullyQualifiedName(d.Get("authentication_policy").(string)).(sdk.SchemaObjectIdentifier) + if !ok { + return fmt.Errorf("authentication_policy %s is not a valid authentication policy qualified name, expected format: `\"db\".\"schema\".\"policy\"`", d.Get("authentication_policy")) + } + + err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + Set: &sdk.AccountSet{ + AuthenticationPolicy: authenticationPolicy, + }, + }) + if err != nil { + return err + } + + d.SetId(helpers.EncodeSnowflakeID(authenticationPolicy)) + + return ReadAccountAuthenticationPolicyAttachment(d, meta) +} + +func ReadAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error { + authenticationPolicy := helpers.DecodeSnowflakeID(d.Id()) + if err := d.Set("authentication_policy", authenticationPolicy.FullyQualifiedName()); err != nil { + return err + } + + return nil +} + +// DeleteAccountAuthenticationPolicyAttachment implements schema.DeleteFunc. +func DeleteAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error { + client := meta.(*provider.Context).Client + ctx := context.Background() + + err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + Unset: &sdk.AccountUnset{ + AuthenticationPolicy: sdk.Bool(true), + }, + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/resources/account_authentication_policy_attachment_acceptance_test.go b/pkg/resources/account_authentication_policy_attachment_acceptance_test.go new file mode 100644 index 0000000000..2ef5a658a4 --- /dev/null +++ b/pkg/resources/account_authentication_policy_attachment_acceptance_test.go @@ -0,0 +1,52 @@ +package resources_test + +import ( + "fmt" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_AccountAuthenticationPolicyAttachment(t *testing.T) { + policyName := acc.TestClient().Ids.Alpha() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: accountAuthenticationPolicyAttachmentConfig(acc.TestDatabaseName, acc.TestSchemaName, policyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("snowflake_account_authentication_policy_attachment.att", "id"), + ), + }, + { + ResourceName: "snowflake_account_authentication_policy_attachment.att", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func accountAuthenticationPolicyAttachmentConfig(databaseName, schemaName, policyName string) string { + s := ` +resource "snowflake_authentication_policy" "pa" { + database = "%s" + schema = "%s" + name = "%v" +} + +resource "snowflake_account_authentication_policy_attachment" "att" { + authentication_policy = snowflake_authentication_policy.pa.fully_qualified_name +} +` + return fmt.Sprintf(s, databaseName, schemaName, policyName) +} diff --git a/pkg/resources/account_password_policy_attachment.go b/pkg/resources/account_password_policy_attachment.go index f362ff1629..245b2d33c2 100644 --- a/pkg/resources/account_password_policy_attachment.go +++ b/pkg/resources/account_password_policy_attachment.go @@ -21,7 +21,7 @@ var accountPasswordPolicyAttachmentSchema = map[string]*schema.Schema{ }, } -// AccountPasswordPolicyAttachment returns a pointer to the resource representing an api integration. +// AccountPasswordPolicyAttachment returns a pointer to the resource representing an account password policy attachment. func AccountPasswordPolicyAttachment() *schema.Resource { return &schema.Resource{ Description: "Specifies the password policy to use for the current account. To set the password policy of a different account, use a provider alias.", diff --git a/pkg/resources/account_password_policy_attachment_acceptance_test.go b/pkg/resources/account_password_policy_attachment_acceptance_test.go index dc43401416..fa42811b22 100644 --- a/pkg/resources/account_password_policy_attachment_acceptance_test.go +++ b/pkg/resources/account_password_policy_attachment_acceptance_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -11,7 +13,7 @@ import ( ) func TestAcc_AccountPasswordPolicyAttachment(t *testing.T) { - prefix := acc.TestClient().Ids.Alpha() + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -22,7 +24,7 @@ func TestAcc_AccountPasswordPolicyAttachment(t *testing.T) { CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: accountPasswordPolicyAttachmentConfig(acc.TestDatabaseName, acc.TestSchemaName, prefix), + Config: accountPasswordPolicyAttachmentConfig(id), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("snowflake_account_password_policy_attachment.att", "id"), ), @@ -44,7 +46,7 @@ func TestAcc_AccountPasswordPolicyAttachment(t *testing.T) { }) } -func accountPasswordPolicyAttachmentConfig(databaseName, schemaName, prefix string) string { +func accountPasswordPolicyAttachmentConfig(id sdk.SchemaObjectIdentifier) string { s := ` resource "snowflake_password_policy" "pa" { database = "%s" @@ -56,5 +58,5 @@ resource "snowflake_account_password_policy_attachment" "att" { password_policy = snowflake_password_policy.pa.fully_qualified_name } ` - return fmt.Sprintf(s, databaseName, schemaName, prefix) + return fmt.Sprintf(s, id.DatabaseName(), id.SchemaName(), id.Name()) } diff --git a/pkg/resources/authentication_policy.go b/pkg/resources/authentication_policy.go new file mode 100644 index 0000000000..9dbdff1098 --- /dev/null +++ b/pkg/resources/authentication_policy.go @@ -0,0 +1,518 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "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" + "reflect" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var authenticationPolicySchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the authentication policy."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "schema": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("The schema in which to create the authentication policy."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "database": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("The database in which to create the authentication policy."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "authentication_methods": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: sdkValidation(sdk.ToAuthenticationMethodsOption), + }, + Optional: true, + Description: fmt.Sprintf("A list of authentication methods that are allowed during login. This parameter accepts one or more of the following values: %s", possibleValuesListed(sdk.AllAuthenticationMethods)), + }, + "mfa_authentication_methods": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: sdkValidation(sdk.ToMfaAuthenticationMethodsOption), + }, + Optional: true, + Description: fmt.Sprintf("A list of authentication methods that enforce multi-factor authentication (MFA) during login. Authentication methods not listed in this parameter do not prompt for multi-factor authentication. Allowed values are %s.", possibleValuesListed(sdk.AllMfaAuthenticationMethods)), + }, + "mfa_enrollment": { + Type: schema.TypeString, + Optional: true, + Description: "Determines whether a user must enroll in multi-factor authentication. Allowed values are REQUIRED and OPTIONAL. When REQUIRED is specified, Enforces users to enroll in MFA. If this value is used, then the CLIENT_TYPES parameter must include SNOWFLAKE_UI, because Snowsight is the only place users can enroll in multi-factor authentication (MFA).", + ValidateDiagFunc: sdkValidation(sdk.ToMfaEnrollmentOption), + Default: "OPTIONAL", + }, + "client_types": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: sdkValidation(sdk.ToClientTypesOption), + }, + Optional: true, + Description: fmt.Sprintf("A list of clients that can authenticate with Snowflake. If a client tries to connect, and the client is not one of the valid CLIENT_TYPES, then the login attempt fails. Allowed values are %s. The CLIENT_TYPES property of an authentication policy is a best effort method to block user logins based on specific clients. It should not be used as the sole control to establish a security boundary.", possibleValuesListed(sdk.AllClientTypes)), + }, + "security_integrations": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + Optional: true, + Description: "A list of security integrations the authentication policy is associated with. This parameter has no effect when SAML or OAUTH are not in the AUTHENTICATION_METHODS list. All values in the SECURITY_INTEGRATIONS list must be compatible with the values in the AUTHENTICATION_METHODS list. For example, if SECURITY_INTEGRATIONS contains a SAML security integration, and AUTHENTICATION_METHODS contains OAUTH, then you cannot create the authentication policy. To allow all security integrations use ALL as parameter.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the authentication policy.", + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW AUTHENTICATION POLICIES` for the given policy.", + Elem: &schema.Resource{ + Schema: schemas.ShowAuthenticationPolicySchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE AUTHENTICATION POLICY` for the given policy.", + Elem: &schema.Resource{ + Schema: schemas.AuthenticationPolicyDescribeSchema, + }, + }, + FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, +} + +// AuthenticationPolicy returns a pointer to the resource representing an authentication policy. +func AuthenticationPolicy() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextAuthenticationPolicy, + ReadContext: ReadContextAuthenticationPolicy, + UpdateContext: UpdateContextAuthenticationPolicy, + DeleteContext: DeleteContextAuthenticationPolicy, + Description: "Resource used to manage authentication policy objects. For more information, check [authentication policy documentation](https://docs.snowflake.com/en/sql-reference/sql/create-authentication-policy).", + + Schema: authenticationPolicySchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportAuthenticationPolicy, + }, + } +} + +func ImportAuthenticationPolicy(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting authentication policy import") + client := meta.(*provider.Context).Client + + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + authenticationPolicy, err := client.AuthenticationPolicies.ShowByID(ctx, id) + if err != nil { + return nil, err + } + + if err = d.Set("name", authenticationPolicy.Name); err != nil { + return nil, err + } + if err = d.Set("database", authenticationPolicy.DatabaseName); err != nil { + return nil, err + } + if err = d.Set("schema", authenticationPolicy.SchemaName); err != nil { + return nil, err + } + if err = d.Set("comment", authenticationPolicy.Comment); err != nil { + return nil, err + } + + // needed as otherwise the resource will be incorrectly imported when a list-parameter value equals a default value + authenticationPolicyDescriptions, err := client.AuthenticationPolicies.Describe(ctx, id) + authenticationMethods := getListParameterFromDescribe(authenticationPolicyDescriptions, "AUTHENTICATION_METHODS") + if err = d.Set("authentication_methods", authenticationMethods); err != nil { + return nil, err + } + mfaAuthenticationMethods := getListParameterFromDescribe(authenticationPolicyDescriptions, "MFA_AUTHENTICATION_METHODS") + if err = d.Set("mfa_authentication_methods", mfaAuthenticationMethods); err != nil { + return nil, err + } + clientTypes := getListParameterFromDescribe(authenticationPolicyDescriptions, "CLIENT_TYPES") + if err = d.Set("client_types", clientTypes); err != nil { + return nil, err + } + securityIntegrations := getListParameterFromDescribe(authenticationPolicyDescriptions, "SECURITY_INTEGRATIONS") + if err = d.Set("security_integrations", securityIntegrations); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextAuthenticationPolicy(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + name := d.Get("name").(string) + databaseName := d.Get("database").(string) + schemaName := d.Get("schema").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + req := sdk.NewCreateAuthenticationPolicyRequest(id) + + // Set optionals + if v, ok := d.GetOk("authentication_methods"); ok { + authenticationMethodsRawList := expandStringList(v.(*schema.Set).List()) + authenticationMethods := make([]sdk.AuthenticationMethods, len(authenticationMethodsRawList)) + for i, v := range authenticationMethodsRawList { + option, err := sdk.ToAuthenticationMethodsOption(v) + if err != nil { + return diag.FromErr(err) + } + authenticationMethods[i] = sdk.AuthenticationMethods{Method: *option} + } + req.WithAuthenticationMethods(authenticationMethods) + } + + if v, ok := d.GetOk("mfa_authentication_methods"); ok { + mfaAuthenticationMethodsRawList := expandStringList(v.(*schema.Set).List()) + mfaAuthenticationMethods := make([]sdk.MfaAuthenticationMethods, len(mfaAuthenticationMethodsRawList)) + for i, v := range mfaAuthenticationMethodsRawList { + option, err := sdk.ToMfaAuthenticationMethodsOption(v) + if err != nil { + return diag.FromErr(err) + } + mfaAuthenticationMethods[i] = sdk.MfaAuthenticationMethods{Method: *option} + } + req.WithMfaAuthenticationMethods(mfaAuthenticationMethods) + } + + if v, ok := d.GetOk("mfa_enrollment"); ok { + option, err := sdk.ToMfaEnrollmentOption(v.(string)) + if err != nil { + return diag.FromErr(err) + } + req = req.WithMfaEnrollment(*option) + } + + if v, ok := d.GetOk("client_types"); ok { + clientTypesRawList := expandStringList(v.(*schema.Set).List()) + clientTypes := make([]sdk.ClientTypes, len(clientTypesRawList)) + for i, v := range clientTypesRawList { + option, err := sdk.ToClientTypesOption(v) + if err != nil { + return diag.FromErr(err) + } + clientTypes[i] = sdk.ClientTypes{ClientType: *option} + } + req.WithClientTypes(clientTypes) + } + + if v, ok := d.GetOk("security_integrations"); ok { + securityIntegrationsRawList := expandStringList(v.(*schema.Set).List()) + securityIntegrations := make([]sdk.SecurityIntegrationsOption, len(securityIntegrationsRawList)) + for i, v := range securityIntegrationsRawList { + securityIntegrations[i] = sdk.SecurityIntegrationsOption{Name: sdk.NewAccountObjectIdentifier(v)} + } + req.WithSecurityIntegrations(securityIntegrations) + } + + if v, ok := d.GetOk("comment"); ok { + req = req.WithComment(v.(string)) + } + + client := meta.(*provider.Context).Client + if err := client.AuthenticationPolicies.Create(ctx, req); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextAuthenticationPolicy(ctx, d, meta) +} + +func ReadContextAuthenticationPolicy(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + authenticationPolicy, err := client.AuthenticationPolicies.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 retrieve authentication policy. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } + return diag.FromErr(err) + } + + authenticationPolicyDescriptions, err := client.AuthenticationPolicies.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + authenticationMethods := getListArgumentWithDefaults(d, "authentication_methods", getListParameterFromDescribe(authenticationPolicyDescriptions, "AUTHENTICATION_METHODS"), []string{"ALL"}) + if err = d.Set("authentication_methods", authenticationMethods); err != nil { + return diag.FromErr(err) + } + + mfaAuthenticationMethods := getListArgumentWithDefaults(d, "mfa_authentication_methods", getListParameterFromDescribe(authenticationPolicyDescriptions, "MFA_AUTHENTICATION_METHODS"), []string{"PASSWORD", "SAML"}) + if err = d.Set("mfa_authentication_methods", mfaAuthenticationMethods); err != nil { + return diag.FromErr(err) + } + + mfaEnrollment, err := collections.FindFirst(authenticationPolicyDescriptions, func(prop sdk.AuthenticationPolicyDescription) bool { return prop.Property == "MFA_ENROLLMENT" }) + if err == nil { + if err = d.Set("mfa_enrollment", mfaEnrollment.Value); err != nil { + return diag.FromErr(err) + } + } + + clientTypes := getListArgumentWithDefaults(d, "client_types", getListParameterFromDescribe(authenticationPolicyDescriptions, "CLIENT_TYPES"), []string{"ALL"}) + if err = d.Set("client_types", clientTypes); err != nil { + return diag.FromErr(err) + } + + securityIntegrations := getListArgumentWithDefaults(d, "security_integrations", getListParameterFromDescribe(authenticationPolicyDescriptions, "SECURITY_INTEGRATIONS"), []string{"ALL"}) + if err = d.Set("security_integrations", securityIntegrations); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("comment", authenticationPolicy.Comment); err != nil { + return diag.FromErr(err) + } + if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { + return diag.FromErr(err) + } + + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.AuthenticationPolicyToSchema(authenticationPolicy)}); err != nil { + return diag.FromErr(err) + } + + if err = d.Set(DescribeOutputAttributeName, []map[string]any{schemas.AuthenticationPolicyDescriptionToSchema(authenticationPolicyDescriptions)}); err != nil { + return diag.FromErr(err) + } + + return diags +} + +func getListParameterFromDescribe(authenticationPolicyDescriptions []sdk.AuthenticationPolicyDescription, parameterName string) []string { + parameterList := make([]string, 0) + if parameterProperty, err := collections.FindFirst(authenticationPolicyDescriptions, func(prop sdk.AuthenticationPolicyDescription) bool { + return prop.Property == parameterName + }); err == nil { + parameterList = append(parameterList, sdk.ParseCommaSeparatedStringArray(parameterProperty.Value, false)...) + } + return parameterList +} + +// getListArgumentWithDefaults returns the list of values for a given argument, with the defaults applied, if necessary. Otherwise, tf plan will always show a diff with a list parameter with defaults when no value is set. +func getListArgumentWithDefaults(d *schema.ResourceData, argumentName string, argumentIs []string, argumentDefaults []string) []string { + // in case nothing is set in the tf resource and the is equals the default, we set the is to empty + argumentShould := d.Get(argumentName).(*schema.Set).List() + if stringSlicesEqual(argumentIs, argumentDefaults) && len(argumentShould) == 0 { + argumentIs = []string{} + } + return argumentIs +} + +func UpdateContextAuthenticationPolicy(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + set, unset := sdk.NewAuthenticationPolicySetRequest(), sdk.NewAuthenticationPolicyUnsetRequest() + + // change to name + if d.HasChange("name") { + newId, err := sdk.ParseSchemaObjectIdentifier(d.Get("name").(string)) + if err != nil { + return diag.FromErr(err) + } + + err = client.AuthenticationPolicies.Alter(ctx, sdk.NewAlterAuthenticationPolicyRequest(id).WithRenameTo(newId)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(newId)) + id = newId + } + + // change to authentication methods + if d.HasChange("authentication_methods") { + if v, ok := d.GetOk("authentication_methods"); ok { + authenticationMethods := expandStringList(v.(*schema.Set).List()) + authenticationMethodsValues := make([]sdk.AuthenticationMethods, len(authenticationMethods)) + for i, v := range authenticationMethods { + option, err := sdk.ToAuthenticationMethodsOption(v) + if err != nil { + return diag.FromErr(err) + } + authenticationMethodsValues[i] = sdk.AuthenticationMethods{Method: *option} + } + + set.WithAuthenticationMethods(authenticationMethodsValues) + } else { + unset.WithAuthenticationMethods(true) + } + } + + // change to mfa authentication methods + if d.HasChange("mfa_authentication_methods") { + if v, ok := d.GetOk("mfa_authentication_methods"); ok { + mfaAuthenticationMethods := expandStringList(v.(*schema.Set).List()) + mfaAuthenticationMethodsValues := make([]sdk.MfaAuthenticationMethods, len(mfaAuthenticationMethods)) + for i, v := range mfaAuthenticationMethods { + option, err := sdk.ToMfaAuthenticationMethodsOption(v) + if err != nil { + return diag.FromErr(err) + } + mfaAuthenticationMethodsValues[i] = sdk.MfaAuthenticationMethods{Method: *option} + } + + set.WithMfaAuthenticationMethods(mfaAuthenticationMethodsValues) + } else { + unset.WithMfaAuthenticationMethods(true) + } + } + + // change to mfa enrollment + if d.HasChange("mfa_enrollment") { + if mfaEnrollmentOption, err := sdk.ToMfaEnrollmentOption(d.Get("mfa_enrollment").(string)); err == nil { + set.WithMfaEnrollment(*mfaEnrollmentOption) + } else { + unset.WithMfaEnrollment(true) + } + } + + // change to client types + if d.HasChange("client_types") { + if v, ok := d.GetOk("client_types"); ok { + clientTypes := expandStringList(v.(*schema.Set).List()) + clientTypesValues := make([]sdk.ClientTypes, len(clientTypes)) + for i, v := range clientTypes { + option, err := sdk.ToClientTypesOption(v) + if err != nil { + return diag.FromErr(err) + } + clientTypesValues[i] = sdk.ClientTypes{ClientType: *option} + } + + set.WithClientTypes(clientTypesValues) + } else { + unset.WithClientTypes(true) + } + } + + // change to security integrations + if d.HasChange("security_integrations") { + if v, ok := d.GetOk("security_integrations"); ok { + securityIntegrations := expandStringList(v.(*schema.Set).List()) + securityIntegrationsValues := make([]sdk.SecurityIntegrationsOption, len(securityIntegrations)) + for i, v := range securityIntegrations { + securityIntegrationsValues[i] = sdk.SecurityIntegrationsOption{Name: sdk.NewAccountObjectIdentifier(v)} + } + + set.WithSecurityIntegrations(securityIntegrationsValues) + } else { + unset.WithSecurityIntegrations(true) + } + } + + // change to comment + if d.HasChange("comment") { + if v, ok := d.GetOk("comment"); ok { + set.Comment = sdk.String(v.(string)) + } else { + unset.WithComment(true) + } + } + + if !reflect.DeepEqual(*set, *sdk.NewAuthenticationPolicySetRequest()) { + req := sdk.NewAlterAuthenticationPolicyRequest(id).WithSet(*set) + if err := client.AuthenticationPolicies.Alter(ctx, req); err != nil { + return diag.FromErr(err) + } + } + + if !reflect.DeepEqual(*unset, *sdk.NewAuthenticationPolicyUnsetRequest()) { + req := sdk.NewAlterAuthenticationPolicyRequest(id).WithUnset(*unset) + if err := client.AuthenticationPolicies.Alter(ctx, req); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextAuthenticationPolicy(ctx, d, meta) +} + +func DeleteContextAuthenticationPolicy(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting authentication policy", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } + } + + if err := client.AuthenticationPolicies.Drop(ctx, sdk.NewDropAuthenticationPolicyRequest(id).WithIfExists(true)); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} + +func stringSlicesEqual(s1 []string, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + + // convert slices to maps for easy comparison + set1 := make(map[string]bool) + for _, v := range s1 { + set1[v] = true + } + + set2 := make(map[string]bool) + for _, v := range s2 { + set2[v] = true + } + + for k, _ := range set1 { + if _, ok := set2[k]; !ok { + return false + } + } + return true +} diff --git a/pkg/resources/authentication_policy_acceptance_test.go b/pkg/resources/authentication_policy_acceptance_test.go new file mode 100644 index 0000000000..95b7cf7e92 --- /dev/null +++ b/pkg/resources/authentication_policy_acceptance_test.go @@ -0,0 +1,78 @@ +package resources_test + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_AuthenticationPolicy(t *testing.T) { + accName := acc.TestClient().Ids.Alpha() + comment := "This is a test resource" + m := func(authenticationMethods []string, mfaAuthenticationMethods []string, mfaEnrollment string, clientTypes []string, securityIntegrations []string) map[string]config.Variable { + authenticationMethodsStringVariables := make([]config.Variable, len(authenticationMethods)) + for i, v := range authenticationMethods { + authenticationMethodsStringVariables[i] = config.StringVariable(v) + } + mfaAuthenticationMethodsStringVariables := make([]config.Variable, len(mfaAuthenticationMethods)) + for i, v := range mfaAuthenticationMethods { + mfaAuthenticationMethodsStringVariables[i] = config.StringVariable(v) + } + clientTypesStringVariables := make([]config.Variable, len(clientTypes)) + for i, v := range clientTypes { + clientTypesStringVariables[i] = config.StringVariable(v) + } + securityIntegrationsStringVariables := make([]config.Variable, len(securityIntegrations)) + for i, v := range securityIntegrations { + securityIntegrationsStringVariables[i] = config.StringVariable(v) + } + + return map[string]config.Variable{ + "name": config.StringVariable(accName), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "authentication_methods": config.SetVariable(authenticationMethodsStringVariables...), + "mfa_authentication_methods": config.SetVariable(mfaAuthenticationMethodsStringVariables...), + "mfa_enrollment": config.StringVariable(mfaEnrollment), + "client_types": config.SetVariable(clientTypesStringVariables...), + "security_integrations": config.SetVariable(securityIntegrationsStringVariables...), + "comment": config.StringVariable(comment), + } + } + variables1 := m([]string{"PASSWORD"}, []string{"PASSWORD"}, "REQUIRED", []string{"SNOWFLAKE_UI"}, []string{"ALL"}) + + 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.AuthenticationPolicy), + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: variables1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "name", accName), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "authentication_methods.0", "PASSWORD"), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "mfa_authentication_methods.0", "PASSWORD"), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "mfa_enrollment", "REQUIRED"), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "client_types.0", "SNOWFLAKE_UI"), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "security_integrations.0", "ALL"), + resource.TestCheckResourceAttr("snowflake_authentication_policy.authentication_policy", "comment", comment), + ), + }, + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: variables1, + ResourceName: "snowflake_authentication_policy.authentication_policy", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/pkg/resources/testdata/TestAcc_AuthenticationPolicy/test.tf b/pkg/resources/testdata/TestAcc_AuthenticationPolicy/test.tf new file mode 100644 index 0000000000..b78725f2d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AuthenticationPolicy/test.tf @@ -0,0 +1,11 @@ +resource "snowflake_authentication_policy" "authentication_policy" { + name = var.name + database = var.database + schema = var.schema + authentication_methods = var.authentication_methods + mfa_authentication_methods = var.mfa_authentication_methods + mfa_enrollment = var.mfa_enrollment + client_types = var.client_types + security_integrations = var.security_integrations + comment = var.comment +} \ No newline at end of file diff --git a/pkg/resources/testdata/TestAcc_AuthenticationPolicy/variables.tf b/pkg/resources/testdata/TestAcc_AuthenticationPolicy/variables.tf new file mode 100644 index 0000000000..4453375033 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AuthenticationPolicy/variables.tf @@ -0,0 +1,35 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "authentication_methods" { + type = set(string) +} + +variable "mfa_authentication_methods" { + type = set(string) +} + +variable "mfa_enrollment" { + type = string +} + +variable "client_types" { + type = set(string) +} + +variable "security_integrations" { + type = set(string) +} + +variable "comment" { + type = string +} diff --git a/pkg/resources/user_authentication_policy_attachment.go b/pkg/resources/user_authentication_policy_attachment.go new file mode 100644 index 0000000000..c29b54880f --- /dev/null +++ b/pkg/resources/user_authentication_policy_attachment.go @@ -0,0 +1,134 @@ +package resources + +import ( + "context" + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var userAuthenticationPolicyAttachmentSchema = map[string]*schema.Schema{ + "user_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "User name of the user you want to attach the authentication policy to", + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "authentication_policy_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Fully qualified name of the authentication policy", + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + }, +} + +// UserAuthenticationPolicyAttachment returns a pointer to the resource representing a user authentication policy attachment. +func UserAuthenticationPolicyAttachment() *schema.Resource { + return &schema.Resource{ + Description: "Specifies the authentication policy to use for a certain user.", + Create: CreateUserAuthenticationPolicyAttachment, + Read: ReadUserAuthenticationPolicyAttachment, + Delete: DeleteUserAuthenticationPolicyAttachment, + Schema: userAuthenticationPolicyAttachmentSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func CreateUserAuthenticationPolicyAttachment(d *schema.ResourceData, meta any) error { + client := meta.(*provider.Context).Client + ctx := context.Background() + + userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(d.Get("user_name").(string)) + authenticationPolicy := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Get("authentication_policy_name").(string)) + + err := client.Users.Alter(ctx, userName, &sdk.AlterUserOptions{ + Set: &sdk.UserSet{ + AuthenticationPolicy: &authenticationPolicy, + }, + }) + if err != nil { + return err + } + + d.SetId(helpers.EncodeResourceIdentifier(userName.FullyQualifiedName(), authenticationPolicy.FullyQualifiedName())) + + return ReadUserAuthenticationPolicyAttachment(d, meta) +} + +func ReadUserAuthenticationPolicyAttachment(d *schema.ResourceData, meta any) error { + client := meta.(*provider.Context).Client + ctx := context.Background() + + parts := helpers.ParseResourceIdentifier(d.Id()) + if len(parts) != 2 { + return fmt.Errorf("required id format 'user_name|authentication_policy_name', but got: '%s'", d.Id()) + } + + // Note: there is no alphanumeric id for an attachment, so we retrieve the authentication policies attached to a certain user. + userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parts[0]) + policyReferences, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(userName, sdk.PolicyEntityDomainUser)) + if err != nil { + return err + } + + authenticationPolicyReferences := make([]sdk.PolicyReference, 0) + for _, policyReference := range policyReferences { + if policyReference.PolicyKind == sdk.PolicyKindAuthenticationPolicy { + authenticationPolicyReferences = append(authenticationPolicyReferences, policyReference) + } + } + + // Note: this should never happen, but just in case: so far, Snowflake only allows one Authentication Policy per user. + if len(authenticationPolicyReferences) > 1 { + return fmt.Errorf("internal error: multiple policy references attached to a user. This should never happen") + } + + // Note: this means the resource has been deleted outside of Terraform. + if len(authenticationPolicyReferences) == 0 { + d.SetId("") + return nil + } + + if err := d.Set("user_name", userName.Name()); err != nil { + return err + } + if err := d.Set( + "authentication_policy_name", + sdk.NewSchemaObjectIdentifier( + *authenticationPolicyReferences[0].PolicyDb, + *authenticationPolicyReferences[0].PolicySchema, + authenticationPolicyReferences[0].PolicyName, + ).FullyQualifiedName()); err != nil { + return err + } + + return err +} + +func DeleteUserAuthenticationPolicyAttachment(d *schema.ResourceData, meta any) error { + client := meta.(*provider.Context).Client + ctx := context.Background() + + userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(d.Get("user_name").(string)) + + err := client.Users.Alter(ctx, userName, &sdk.AlterUserOptions{ + Unset: &sdk.UserUnset{ + AuthenticationPolicy: sdk.Bool(true), + }, + }) + if err != nil { + return err + } + + d.SetId("") + + return nil +} diff --git a/pkg/resources/user_authentication_policy_attachment_acceptance_test.go b/pkg/resources/user_authentication_policy_attachment_acceptance_test.go new file mode 100644 index 0000000000..99222e2179 --- /dev/null +++ b/pkg/resources/user_authentication_policy_attachment_acceptance_test.go @@ -0,0 +1,74 @@ +package resources_test + +import ( + "fmt" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_UserAuthenticationPolicyAttachment(t *testing.T) { + // TODO [SNOW-1423486]: unskip + t.Skipf("Skip because error %s; will be fixed in SNOW-1423486", "Error: 000606 (57P03): No active warehouse selected in the current session. Select an active warehouse with the 'use warehouse' command.") + userId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + userName := userId.Name() + newUserId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newUserName := newUserId.Name() + authenticationPolicyId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + authenticationPolicyName := authenticationPolicyId.Name() + newAuthenticationPolicyId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + newAuthenticationPolicyName := newAuthenticationPolicyId.Name() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: acc.CheckUserAuthenticationPolicyAttachmentDestroy(t), + Steps: []resource.TestStep{ + // CREATE + { + Config: userAuthenticationPolicyAttachmentConfig(userName, acc.TestDatabaseName, acc.TestSchemaName, authenticationPolicyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "user_name", userName), + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "authentication_policy_name", authenticationPolicyId.FullyQualifiedName()), + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "id", fmt.Sprintf("%s|%s", userId.FullyQualifiedName(), authenticationPolicyId.FullyQualifiedName())), + ), + }, + // UPDATE + { + Config: userAuthenticationPolicyAttachmentConfig(newUserName, acc.TestDatabaseName, acc.TestSchemaName, newAuthenticationPolicyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "user_name", newUserName), + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "authentication_policy_name", newAuthenticationPolicyId.FullyQualifiedName()), + resource.TestCheckResourceAttr("snowflake_user_authentication_policy_attachment.ppa", "id", fmt.Sprintf("%s|%s", userId.FullyQualifiedName(), newAuthenticationPolicyId.FullyQualifiedName())), + ), + }, + // IMPORT + { + ResourceName: "snowflake_user_authentication_policy_attachment.ppa", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func userAuthenticationPolicyAttachmentConfig(userName, databaseName, schemaName, authenticationPolicyName string) string { + return fmt.Sprintf(` +resource "snowflake_user" "user" { + name = "%s" +} + +resource "snowflake_authentication_policy" "ap" { + database = "%s" + schema = "%s" + name = "%s" +} + +resource "snowflake_user_authentication_policy_attachment" "apa" { + authentication_policy_name = snowflake_authentication_policy.ap.fully_qualified_name + user_name = snowflake_user.user.name +} +`, userName, databaseName, schemaName, authenticationPolicyName) +} diff --git a/pkg/resources/user_password_policy_attachment.go b/pkg/resources/user_password_policy_attachment.go index 5ec96deebb..96bac9523a 100644 --- a/pkg/resources/user_password_policy_attachment.go +++ b/pkg/resources/user_password_policy_attachment.go @@ -28,6 +28,7 @@ var userPasswordPolicyAttachmentSchema = map[string]*schema.Schema{ }, } +// UserPasswordPolicyAttachment returns a pointer to the resource representing a user password policy attachment. func UserPasswordPolicyAttachment() *schema.Resource { return &schema.Resource{ Description: "Specifies the password policy to use for a certain user.", diff --git a/pkg/schemas/authentication_policy.go b/pkg/schemas/authentication_policy.go new file mode 100644 index 0000000000..66b96a5eed --- /dev/null +++ b/pkg/schemas/authentication_policy.go @@ -0,0 +1,48 @@ +package schemas + +import ( + "log" + "slices" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// AuthenticationPolicyDescribeSchema represents output of DESCRIBE query for the single AuthenticationPolicy. +var AuthenticationPolicyDescribeSchema = map[string]*schema.Schema{ + "name": {Type: schema.TypeString, Computed: true}, + "owner": {Type: schema.TypeString, Computed: true}, + "authentication_methods": {Type: schema.TypeString, Computed: true}, + "mfa_authentication_methods": {Type: schema.TypeString, Computed: true}, + "mfa_enrollment": {Type: schema.TypeString, Computed: true}, + "client_types": {Type: schema.TypeString, Computed: true}, + "security_integrations": {Type: schema.TypeString, Computed: true}, + "comment": {Type: schema.TypeString, Computed: true}, +} + +var _ = AuthenticationPolicyDescribeSchema + +var AuthenticationPolicyNames = []string{ + "NAME", + "OWNER", + "COMMENT", + "AUTHENTICATION_METHODS", + "CLIENT_TYPES", + "SECURITY_INTEGRATIONS", + "MFA_ENROLLMENT", + "MFA_AUTHENTICATION_METHODS", +} + +func AuthenticationPolicyDescriptionToSchema(authenticationPolicyDescription []sdk.AuthenticationPolicyDescription) map[string]any { + authenticationPolicySchema := make(map[string]any) + for _, property := range authenticationPolicyDescription { + property := property + if slices.Contains(AuthenticationPolicyNames, property.Property) { + authenticationPolicySchema[strings.ToLower(property.Property)] = property.Value + } else { + log.Printf("[WARN] unexpected property %v in authentication policy returned from Snowflake", property.Value) + } + } + return authenticationPolicySchema +} diff --git a/pkg/schemas/authentication_policy_gen.go b/pkg/schemas/authentication_policy_gen.go new file mode 100644 index 0000000000..ed487e4439 --- /dev/null +++ b/pkg/schemas/authentication_policy_gen.go @@ -0,0 +1,61 @@ +// Code generated by sdk-to-schema generator; DO NOT EDIT. + +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// ShowAuthenticationPolicySchema represents output of SHOW query for the single AuthenticationPolicy. +var ShowAuthenticationPolicySchema = map[string]*schema.Schema{ + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "database_name": { + Type: schema.TypeString, + Computed: true, + }, + "schema_name": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "owner_role_type": { + Type: schema.TypeString, + Computed: true, + }, + "options": { + Type: schema.TypeString, + Computed: true, + }, +} + +var _ = ShowAuthenticationPolicySchema + +func AuthenticationPolicyToSchema(authenticationPolicy *sdk.AuthenticationPolicy) map[string]any { + authenticationPolicySchema := make(map[string]any) + authenticationPolicySchema["created_on"] = authenticationPolicy.CreatedOn + authenticationPolicySchema["name"] = authenticationPolicy.Name + authenticationPolicySchema["comment"] = authenticationPolicy.Comment + authenticationPolicySchema["database_name"] = authenticationPolicy.DatabaseName + authenticationPolicySchema["schema_name"] = authenticationPolicy.SchemaName + authenticationPolicySchema["owner"] = authenticationPolicy.Owner + authenticationPolicySchema["owner_role_type"] = authenticationPolicy.OwnerRoleType + authenticationPolicySchema["options"] = authenticationPolicy.Options + return authenticationPolicySchema +} + +var _ = AuthenticationPolicyToSchema diff --git a/pkg/schemas/gen/sdk_show_result_structs.go b/pkg/schemas/gen/sdk_show_result_structs.go index 337367a112..f088d4be01 100644 --- a/pkg/schemas/gen/sdk_show_result_structs.go +++ b/pkg/schemas/gen/sdk_show_result_structs.go @@ -9,6 +9,7 @@ var SdkShowResultStructs = []any{ sdk.ApplicationPackage{}, sdk.ApplicationRole{}, sdk.Application{}, + sdk.AuthenticationPolicy{}, sdk.DatabaseRole{}, sdk.Database{}, sdk.DynamicTable{}, diff --git a/pkg/sdk/authentication_policies_def.go b/pkg/sdk/authentication_policies_def.go index 2d283c45dd..1c53e54eeb 100644 --- a/pkg/sdk/authentication_policies_def.go +++ b/pkg/sdk/authentication_policies_def.go @@ -1,6 +1,10 @@ package sdk -import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" +import ( + "fmt" + g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" + "strings" +) //go:generate go run ./poc/main.go @@ -185,3 +189,49 @@ var AuthenticationPoliciesDef = g.NewInterface( Name(). WithValidation(g.ValidIdentifier, "name"), ) + +func ToAuthenticationMethodsOption(s string) (*AuthenticationMethodsOption, error) { + switch authenticationMethodsOption := AuthenticationMethodsOption(strings.ToUpper(s)); authenticationMethodsOption { + case AuthenticationMethodsAll, + AuthenticationMethodsSaml, + AuthenticationMethodsPassword, + AuthenticationMethodsOauth, + AuthenticationMethodsKeyPair: + return &authenticationMethodsOption, nil + default: + return nil, fmt.Errorf("invalid authentication method type: %s", s) + } +} + +func ToMfaAuthenticationMethodsOption(s string) (*MfaAuthenticationMethodsOption, error) { + switch mfaAuthenticationMethodsOption := MfaAuthenticationMethodsOption(strings.ToUpper(s)); mfaAuthenticationMethodsOption { + case MfaAuthenticationMethodsAll, + MfaAuthenticationMethodsSaml, + MfaAuthenticationMethodsPassword: + return &mfaAuthenticationMethodsOption, nil + default: + return nil, fmt.Errorf("invalid MFA authentication method type: %s", s) + } +} + +func ToMfaEnrollmentOption(s string) (*MfaEnrollmentOption, error) { + switch mfaEnrollmentOption := MfaEnrollmentOption(strings.ToUpper(s)); mfaEnrollmentOption { + case MfaEnrollmentRequired, + MfaEnrollmentOptional: + return &mfaEnrollmentOption, nil + default: + return nil, fmt.Errorf("invalid enrollment option type: %s", s) + } +} + +func ToClientTypesOption(s string) (*ClientTypesOption, error) { + switch clientTypesOption := ClientTypesOption(strings.ToUpper(s)); clientTypesOption { + case ClientTypesAll, + ClientTypesSnowflakeUi, + ClientTypesDrivers, + ClientTypesSnowSql: + return &clientTypesOption, nil + default: + return nil, fmt.Errorf("invalid client type: %s", s) + } +}