diff --git a/.changelog/6472.txt b/.changelog/6472.txt new file mode 100644 index 0000000000..3a8bc6f86f --- /dev/null +++ b/.changelog/6472.txt @@ -0,0 +1,4 @@ +```release-note:enhancement +sql: added `password_policy` field to `google_sql_user` resource + +``` diff --git a/google/resource_sql_user.go b/google/resource_sql_user.go index c0ea400b09..bad1197033 100644 --- a/google/resource_sql_user.go +++ b/google/resource_sql_user.go @@ -105,6 +105,54 @@ func resourceSqlUser() *schema.Resource { }, }, + "password_policy": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_failed_attempts": { + Type: schema.TypeInt, + Optional: true, + Description: `Number of failed attempts allowed before the user get locked.`, + }, + "password_expiration_duration": { + Type: schema.TypeString, + Optional: true, + Description: `Password expiration duration with one week grace period.`, + }, + "enable_failed_attempts_check": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, the check that will lock user after too many failed login attempts will be enabled.`, + }, + "enable_password_verification": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, the user must specify the current password before changing the password. This flag is supported only for MySQL.`, + }, + "status": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "locked": { + Type: schema.TypeBool, + Computed: true, + Description: `If true, user does not have login privileges.`, + }, + "password_expiration_time": { + Type: schema.TypeString, + Computed: true, + Description: `Password expiration duration with one week grace period.`, + }, + }, + }, + }, + }, + }, + }, + "project": { Type: schema.TypeString, Optional: true, @@ -142,6 +190,30 @@ func expandSqlServerUserDetails(cfg interface{}) (*sqladmin.SqlServerUserDetails } +func expandPasswordPolicy(cfg interface{}) *sqladmin.UserPasswordValidationPolicy { + if len(cfg.([]interface{})) == 0 || cfg.([]interface{})[0] == nil { + return nil + } + raw := cfg.([]interface{})[0].(map[string]interface{}) + + upvp := &sqladmin.UserPasswordValidationPolicy{} + + if v, ok := raw["allowed_failed_attempts"]; ok { + upvp.AllowedFailedAttempts = int64(v.(int)) + } + if v, ok := raw["password_expiration_duration"]; ok { + upvp.PasswordExpirationDuration = v.(string) + } + if v, ok := raw["enable_failed_attempts_check"]; ok { + upvp.EnableFailedAttemptsCheck = v.(bool) + } + if v, ok := raw["enable_password_verification"]; ok { + upvp.EnablePasswordVerification = v.(bool) + } + + return upvp +} + func resourceSqlUserCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) userAgent, err := generateUserAgentString(d, config.userAgent) @@ -176,6 +248,11 @@ func resourceSqlUserCreate(d *schema.ResourceData, meta interface{}) error { user.SqlserverUserDetails = ssud } + if v, ok := d.GetOk("password_policy"); ok { + pp := expandPasswordPolicy(v) + user.PasswordPolicy = pp + } + mutexKV.Lock(instanceMutexKey(project, instance)) defer mutexKV.Unlock(instanceMutexKey(project, instance)) var op *sqladmin.Operation @@ -282,10 +359,58 @@ func resourceSqlUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting server_roles: %s", err) } } + + if user.PasswordPolicy != nil { + passwordPolicy := flattenPasswordPolicy(user.PasswordPolicy) + if len(passwordPolicy.([]map[string]interface{})[0]) != 0 { + if err := d.Set("password_policy", passwordPolicy); err != nil { + return fmt.Errorf("Error setting password_policy: %s", err) + } + } + } d.SetId(fmt.Sprintf("%s/%s/%s", user.Name, user.Host, user.Instance)) return nil } +func flattenPasswordPolicy(passwordPolicy *sqladmin.UserPasswordValidationPolicy) interface{} { + data := map[string]interface{}{} + if passwordPolicy.AllowedFailedAttempts != 0 { + data["allowed_failed_attempts"] = passwordPolicy.AllowedFailedAttempts + } + + if passwordPolicy.EnableFailedAttemptsCheck != false { + data["enable_failed_attempts_check"] = passwordPolicy.EnableFailedAttemptsCheck + } + + if passwordPolicy.EnablePasswordVerification != false { + data["enable_password_verification"] = passwordPolicy.EnablePasswordVerification + } + if len(passwordPolicy.PasswordExpirationDuration) != 0 { + data["password_expiration_duration"] = passwordPolicy.PasswordExpirationDuration + } + + if passwordPolicy.Status != nil { + status := flattenPasswordStatus(passwordPolicy.Status) + if len(status.([]map[string]interface{})[0]) != 0 { + data["status"] = flattenPasswordStatus(passwordPolicy.Status) + } + } + + return []map[string]interface{}{data} +} + +func flattenPasswordStatus(status *sqladmin.PasswordStatus) interface{} { + data := map[string]interface{}{} + if status.Locked != false { + data["locked"] = status.Locked + } + if len(status.PasswordExpirationTime) != 0 { + data["password_expiration_time"] = status.PasswordExpirationTime + } + + return []map[string]interface{}{data} +} + func resourceSqlUserUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) userAgent, err := generateUserAgentString(d, config.userAgent) @@ -293,7 +418,7 @@ func resourceSqlUserUpdate(d *schema.ResourceData, meta interface{}) error { return err } - if d.HasChange("password") { + if d.HasChange("password") || d.HasChange("password_policy") { project, err := getProject(d, config) if err != nil { return err @@ -317,6 +442,7 @@ func resourceSqlUserUpdate(d *schema.ResourceData, meta interface{}) error { } user.SqlserverUserDetails = ssud } + user.PasswordPolicy = expandPasswordPolicy(d.Get("password_policy")) mutexKV.Lock(instanceMutexKey(project, instance)) defer mutexKV.Unlock(instanceMutexKey(project, instance)) diff --git a/google/resource_sql_user_test.go b/google/resource_sql_user_test.go index 3108c38fec..e55e6b8712 100644 --- a/google/resource_sql_user_test.go +++ b/google/resource_sql_user_test.go @@ -279,6 +279,43 @@ func testAccSqlUserDestroyProducer(t *testing.T) func(s *terraform.State) error } } +func TestAccSqlUser_mysqlPasswordPolicy(t *testing.T) { + // Multiple fine-grained resources + skipIfVcr(t) + t.Parallel() + + instance := fmt.Sprintf("tf-test-i%d", randInt(t)) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlUserDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlUser_mysqlPasswordPolicy(instance, "password", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_sql_user.user2"), + ), + }, + { + // Update password + Config: testGoogleSqlUser_mysqlPasswordPolicy(instance, "new_password", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_sql_user.user2"), + ), + }, + { + ResourceName: "google_sql_user.user2", + ImportStateId: fmt.Sprintf("%s/%s/gmail.com/admin", getTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + func testGoogleSqlUser_mysql(instance, password string, disabled bool) string { return fmt.Sprintf(` resource "google_sql_database_instance" "instance" { @@ -312,6 +349,48 @@ resource "google_sql_user" "user2" { `, instance, password, disabled) } +func testGoogleSqlUser_mysqlPasswordPolicy(instance, password string, disabled bool) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_8_0" + deletion_protection = false + settings { + tier = "db-f1-micro" + } +} + +resource "google_sql_user" "user1" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "google.com" + password = "%s" + sql_server_user_details { + disabled = "%t" + server_roles = [ "admin" ] + } + password_policy { + allowed_failed_attempts = 6 + password_expiration_duration = "2592000s" + enable_failed_attempts_check = true + enable_password_verification = true + } +} + +resource "google_sql_user" "user2" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "gmail.com" + password = "hunter2" + password_policy { + allowed_failed_attempts = 6 + enable_failed_attempts_check = true + } +} +`, instance, password, disabled) +} + func testGoogleSqlUser_postgres(instance, password string) string { return fmt.Sprintf(` resource "google_sql_database_instance" "instance" { diff --git a/website/docs/r/sql_user.html.markdown b/website/docs/r/sql_user.html.markdown index 784e837b21..3e4d2cc59c 100644 --- a/website/docs/r/sql_user.html.markdown +++ b/website/docs/r/sql_user.html.markdown @@ -100,6 +100,22 @@ The following arguments are supported: * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +The optional `password_policy` block is only supported by Mysql. The `password_policy` block supports: + +* `allowed_failed_attempts` - (Optional) Number of failed attempts allowed before the user get locked. + +* `password_expiration_duration` - (Optional) Password expiration duration with one week grace period. + +* `enable_failed_attempts_check` - (Optional) If true, the check that will lock user after too many failed login attempts will be enabled. + +* `enable_password_verification` - (Optional) If true, the user must specify the current password before changing the password. This flag is supported only for MySQL. + +The read only `password_policy.status` subblock supports: + +* `locked` - (read only) If true, user does not have login privileges. + +* `password_expiration_time` - (read only) Password expiration duration with one week grace period. + ## Attributes Reference Only the arguments listed above are exposed as attributes.