From 74056c3f3218c5e6727f129f0d9644e6bd0767b0 Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Tue, 17 Jun 2025 20:57:07 +0200 Subject: [PATCH 1/7] hex hash - initial --- mysql/resource_user.go | 63 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index bf808c1d..c02fd261 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -95,7 +95,13 @@ func resourceUser() *schema.Resource { DiffSuppressFunc: NewEmptyStringSuppressFunc, ConflictsWith: []string{"plaintext_password", "password"}, }, - + "auth_string_hex": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + DiffSuppressFunc: NewEmptyStringSuppressFunc, + ConflictsWith: []string{"plaintext_password", "password", "auth_string_hashed"}, + }, "tls_option": { Type: schema.TypeString, Optional: true, @@ -171,6 +177,29 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d authStm = fmt.Sprintf("%s AS ?", authStm) } } + var hashed_hex string + if v, ok := d.GetOk("auth_string_hex"); ok { + hashed_hex = v.(string) + if hashed_hex != "" { + if hashed != "" { + return diag.Errorf("can not specify both auth_string_hashed and auth_string_hex") + } + if authStm == "" { + return diag.Errorf("auth_string_hex is not supported for auth plugin %s", auth) + } + if strings.HasPrefix(hashed_hex, "0x") || strings.HasPrefix(hashed_hex, "0X") { + hashed_hex = hashed_hex[2:] + } + + if err := validateHexString(hashed_hex); err != nil { + return diag.Errorf("invalid hex string for auth_string_hex: %v", err) + } + authStm = fmt.Sprintf("%s AS 0x%s", authStm, hashed_hex) + } + + } + user := d.Get("user").(string) + host := d.Get("host").(string) var stmtSQL string var args []interface{} @@ -180,15 +209,15 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d if aadIdentity["type"].(string) == "service_principal" { // CREATE AADUSER 'mysqlProtocolLoginName"@"mysqlHostRestriction' IDENTIFIED BY 'identityId' stmtSQL = "CREATE AADUSER ?@? IDENTIFIED BY ?" - args = []interface{}{d.Get("user").(string), d.Get("host").(string), aadIdentity["identity"].(string)} + args = []interface{}{user, host, aadIdentity["identity"].(string)} } else { // CREATE AADUSER 'identityName"@"mysqlHostRestriction' AS 'mysqlProtocolLoginName' stmtSQL = "CREATE AADUSER ?@? AS ?" - args = []interface{}{aadIdentity["identity"].(string), d.Get("host").(string), d.Get("user").(string)} + args = []interface{}{aadIdentity["identity"].(string), host, user} } } else { stmtSQL = "CREATE USER ?@?" - args = []interface{}{d.Get("user").(string), d.Get("host").(string)} + args = []interface{}{user, host} } var password string @@ -198,7 +227,7 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d password = d.Get("password").(string) } - if auth == "AWSAuthenticationPlugin" && d.Get("host").(string) == "localhost" { + if auth == "AWSAuthenticationPlugin" && host == "localhost" { return diag.Errorf("cannot use IAM auth against localhost") } @@ -223,7 +252,7 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d if getVersionFromMeta(ctx, meta).GreaterThan(requiredVersion) && d.Get("tls_option").(string) != "" { if createObj == "AADUSER" { updateStmtSql = "ALTER USER ?@? REQUIRE " + d.Get("tls_option").(string) - updateArgs = []interface{}{d.Get("user").(string), d.Get("host").(string)} + updateArgs = []interface{}{user, host} } else { stmtSQL += " REQUIRE " + d.Get("tls_option").(string) } @@ -246,8 +275,8 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d return diag.Errorf("failed executing SQL: %v", err) } - user := fmt.Sprintf("%s@%s", d.Get("user").(string), d.Get("host").(string)) - d.SetId(user) + userId := fmt.Sprintf("%s@%s", user, host) + d.SetId(userId) if updateStmtSql != "" { log.Println("[DEBUG] Executing statement:", updateStmtSql, "args:", updateArgs) @@ -527,3 +556,21 @@ func NewEmptyStringSuppressFunc(k, old, new string, d *schema.ResourceData) bool return false } + +func validateHexString(hexStr string) error { + if len(hexStr) == 0 { + return fmt.Errorf("hex string cannot be empty") + } + + if len(hexStr)%2 != 0 { + return fmt.Errorf("hex string must have even length") + } + + for i, char := range strings.ToLower(hexStr) { + if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { + return fmt.Errorf("invalid hex character '%c' at position %d", char, i) + } + } + + return nil +} From 6f2d6f2a535cd91ff8305e016a126c9b53016d11 Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Wed, 18 Jun 2025 22:37:38 +0200 Subject: [PATCH 2/7] support for auth_string_hex --- mysql/resource_user.go | 44 +++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index c02fd261..b797594d 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -315,14 +315,24 @@ func UpdateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d auth = v.(string) } if len(auth) > 0 { - if d.HasChange("tls_option") || d.HasChange("auth_plugin") || d.HasChange("auth_string_hashed") { + if d.HasChange("tls_option") || d.HasChange("auth_plugin") || d.HasChange("auth_string_hashed") || d.HasChange("auth_string_hex") { var stmtSQL string authString := "" if d.Get("auth_string_hashed").(string) != "" { authString = fmt.Sprintf("IDENTIFIED WITH %s AS '%s'", d.Get("auth_plugin"), d.Get("auth_string_hashed")) + } else if d.Get("auth_string_hex").(string) != "" { + hexValue := d.Get("auth_string_hex").(string) + if strings.HasPrefix(hexValue, "0x") || strings.HasPrefix(hexValue, "0X") { + hexValue = hexValue[2:] + } + if err := validateHexString(hexValue); err != nil { + return diag.Errorf("invalid hex string for auth_string_hex: %v", err) + } + + authString = fmt.Sprintf("IDENTIFIED WITH %s AS 0x%s", d.Get("auth_plugin"), hexValue) } - stmtSQL = fmt.Sprintf("ALTER USER '%s'@'%s' %s REQUIRE %s", + stmtSQL = fmt.Sprintf("ALTER USER `%s`@`%s` %s REQUIRE %s", d.Get("user").(string), d.Get("host").(string), authString, @@ -414,10 +424,15 @@ func ReadUser(ctx context.Context, d *schema.ResourceData, meta interface{}) dia } requiredVersion, _ := version.NewVersion("5.7.0") if getVersionFromMeta(ctx, meta).GreaterThan(requiredVersion) { - stmt := "SHOW CREATE USER ?@?" + _, err := db.ExecContext(ctx, "SET print_identified_with_as_hex = ON") + if err != nil { + // return diag.Errorf("failed setting print_identified_with_as_hex: %v", err) + log.Printf("[DEBUG] Could not set print_identified_with_as_hex: %v", err) + } + stmt := "SHOW CREATE USER ?@?" var createUserStmt string - err := db.QueryRowContext(ctx, stmt, d.Get("user").(string), d.Get("host").(string)).Scan(&createUserStmt) + err = db.QueryRowContext(ctx, stmt, d.Get("user").(string), d.Get("host").(string)).Scan(&createUserStmt) if err != nil { errorNumber := mysqlErrorNumber(err) if errorNumber == unknownUserErrCode || errorNumber == userNotFoundErrCode { @@ -426,17 +441,17 @@ func ReadUser(ctx context.Context, d *schema.ResourceData, meta interface{}) dia } return diag.Errorf("failed getting user: %v", err) } - // Examples of create user: // CREATE USER 'some_app'@'%' IDENTIFIED WITH 'mysql_native_password' AS '*0something' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK // CREATE USER `jdoe-tf-test-47`@`example.com` IDENTIFIED WITH 'caching_sha2_password' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT // CREATE USER `jdoe`@`example.com` IDENTIFIED WITH 'caching_sha2_password' AS '$A$005$i`xay#fG/\' TrbkNA82' REQUIRE NONE PASSWORD - re := regexp.MustCompile("^CREATE USER ['`]([^'`]*)['`]@['`]([^'`]*)['`] IDENTIFIED WITH ['`]([^'`]*)['`] (?:AS '((?:.*?[^\\\\])?)' )?REQUIRE ([^ ]*)") - if m := re.FindStringSubmatch(createUserStmt); len(m) == 6 { + // CREATE USER `hashed_hex`@`localhost` IDENTIFIED WITH 'caching_sha2_password' AS 0x244124303035242522434C16580334755221766C29210D2C415E033550367655494F314864686775414E735A742E6F474857504B623172525066574D524F30506B7A79646F30 REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT + re := regexp.MustCompile("^CREATE USER ['`]([^'`]*)['`]@['`]([^'`]*)['`] IDENTIFIED WITH ['`]([^'`]*)['`] (?:AS (?:'((?:.*?[^\\\\])?)'|(0x[0-9A-Fa-f]+)) )?REQUIRE ([^ ]*)") + if m := re.FindStringSubmatch(createUserStmt); len(m) == 7 { d.Set("user", m[1]) d.Set("host", m[2]) d.Set("auth_plugin", m[3]) - d.Set("tls_option", m[5]) + d.Set("tls_option", m[6]) if m[3] == "aad_auth" { // AADGroup:98e61c8d-e104-4f8c-b1a6-7ae873617fe6:upn:Doe_Family_Group @@ -473,7 +488,18 @@ func ReadUser(ctx context.Context, d *schema.ResourceData, meta interface{}) dia return diag.Errorf("AAD identity couldn't be parsed - it is %s", m[4]) } } else { - d.Set("auth_string_hashed", m[4]) + quotedAuthString := m[4] + hexAuthString := m[5] + if hexAuthString != "" { + d.Set("auth_string_hex", hexAuthString) + d.Set("auth_string_hashed", "") + } else if quotedAuthString != "" { + d.Set("auth_string_hashed", quotedAuthString) + d.Set("auth_string_hex", "") + } else { + d.Set("auth_string_hashed", "") + d.Set("auth_string_hex", "") + } } return nil } From 2bbdc413ec775d64de97a68e0e1bc3bd5fb7b840 Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Fri, 20 Jun 2025 23:22:37 +0200 Subject: [PATCH 3/7] optional 0x prefix --- mysql/resource_user.go | 72 +++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index b797594d..f27076bb 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -99,7 +99,7 @@ func resourceUser() *schema.Resource { Type: schema.TypeString, Optional: true, Sensitive: true, - DiffSuppressFunc: NewEmptyStringSuppressFunc, + DiffSuppressFunc: SuppressHexStringDiff, ConflictsWith: []string{"plaintext_password", "password", "auth_string_hashed"}, }, "tls_option": { @@ -177,24 +177,23 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d authStm = fmt.Sprintf("%s AS ?", authStm) } } - var hashed_hex string + var hashedHex string if v, ok := d.GetOk("auth_string_hex"); ok { - hashed_hex = v.(string) - if hashed_hex != "" { + hashedHex = v.(string) + if hashedHex != "" { if hashed != "" { return diag.Errorf("can not specify both auth_string_hashed and auth_string_hex") } if authStm == "" { return diag.Errorf("auth_string_hex is not supported for auth plugin %s", auth) } - if strings.HasPrefix(hashed_hex, "0x") || strings.HasPrefix(hashed_hex, "0X") { - hashed_hex = hashed_hex[2:] - } + normalizedHex := normalizeHexString(hashedHex) + hexDigits := normalizedHex[2:] // Remove the "0x" prefix for validation - if err := validateHexString(hashed_hex); err != nil { + if err := validateHexString(hexDigits); err != nil { return diag.Errorf("invalid hex string for auth_string_hex: %v", err) } - authStm = fmt.Sprintf("%s AS 0x%s", authStm, hashed_hex) + authStm = fmt.Sprintf("%s AS 0x%s", authStm, hexDigits) } } @@ -322,15 +321,14 @@ func UpdateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d if d.Get("auth_string_hashed").(string) != "" { authString = fmt.Sprintf("IDENTIFIED WITH %s AS '%s'", d.Get("auth_plugin"), d.Get("auth_string_hashed")) } else if d.Get("auth_string_hex").(string) != "" { - hexValue := d.Get("auth_string_hex").(string) - if strings.HasPrefix(hexValue, "0x") || strings.HasPrefix(hexValue, "0X") { - hexValue = hexValue[2:] - } - if err := validateHexString(hexValue); err != nil { + authStringHex := d.Get("auth_string_hex").(string) + normalizedHex := normalizeHexString(authStringHex) + + hexDigits := normalizedHex[2:] + if err := validateHexString(hexDigits); err != nil { return diag.Errorf("invalid hex string for auth_string_hex: %v", err) } - - authString = fmt.Sprintf("IDENTIFIED WITH %s AS 0x%s", d.Get("auth_plugin"), hexValue) + authString = fmt.Sprintf("IDENTIFIED WITH %s AS 0x%s", d.Get("auth_plugin"), hexDigits) } stmtSQL = fmt.Sprintf("ALTER USER `%s`@`%s` %s REQUIRE %s", d.Get("user").(string), @@ -489,9 +487,11 @@ func ReadUser(ctx context.Context, d *schema.ResourceData, meta interface{}) dia } } else { quotedAuthString := m[4] - hexAuthString := m[5] - if hexAuthString != "" { - d.Set("auth_string_hex", hexAuthString) + authStringHex := m[5] + + if authStringHex != "" { + normalizedHex := normalizeHexString(authStringHex) + d.Set("auth_string_hex", normalizedHex) d.Set("auth_string_hashed", "") } else if quotedAuthString != "" { d.Set("auth_string_hashed", quotedAuthString) @@ -582,6 +582,22 @@ func NewEmptyStringSuppressFunc(k, old, new string, d *schema.ResourceData) bool return false } +func SuppressHexStringDiff(k, old, new string, d *schema.ResourceData) bool { + if new == "" { + return true + } + + // Normalize both values and compare + normalizedOld := normalizeHexString(old) + normalizedNew := normalizeHexString(new) + + // Suppress diff if they're the same after normalization + if normalizedOld == normalizedNew { + return true + } + + return false +} func validateHexString(hexStr string) error { if len(hexStr) == 0 { @@ -600,3 +616,21 @@ func validateHexString(hexStr string) error { return nil } + +// Add this helper function to normalize hex strings +func normalizeHexString(hexStr string) string { + if hexStr == "" { + return "" + } + + // Remove 0x prefix if present + if strings.HasPrefix(hexStr, "0x") || strings.HasPrefix(hexStr, "0X") { + hexStr = hexStr[2:] + } + + // Convert to lowercase for consistency + hexStr = strings.ToUpper(hexStr) + + // Always return with 0x prefix for consistency + return "0x" + hexStr +} From fe7773adf280964d42646441fc2e61b3a010faf5 Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Sat, 21 Jun 2025 00:03:14 +0200 Subject: [PATCH 4/7] add tests --- mysql/resource_user_test.go | 107 ++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index 71d332d6..fdf93f4f 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -119,6 +119,63 @@ func TestAccUser_auth_mysql8(t *testing.T) { resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), ), }, + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_no_prefix, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330"), + ), + }, + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_with_prefix, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330"), + ), + }, + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_updated, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239"), + ), + }, + }, + }) +} + +func TestAccUser_auth_mysql8_validation(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckSkipTiDB(t) + testAccPreCheckSkipMariaDB(t) + testAccPreCheckSkipRds(t) + testAccPreCheckSkipNotMySQLVersionMin(t, "8.0.14") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_invalid, + ExpectError: regexp.MustCompile(`invalid hex character 'g'`), + }, + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_odd_length, + ExpectError: regexp.MustCompile(`hex string must have even length`), + }, + { + Config: testAccUserConfig_auth_both_string_fields, + ExpectError: regexp.MustCompile(`can not specify both auth_string_hashed and auth_string_hex`), + }, }, }) } @@ -526,6 +583,56 @@ resource "mysql_user" "test" { } ` +const testAccUserConfig_auth_caching_sha2_password_hex_no_prefix = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330" +} +` +const testAccUserConfig_auth_caching_sha2_password_hex_with_prefix = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330" +} +` +const testAccUserConfig_auth_caching_sha2_password_hex_updated = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239" +} +` +const testAccUserConfig_auth_caching_sha2_password_hex_invalid = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "0x244124303035246g4f1e0d5d1631594f5c56701f3d327d073a724c706273307a5965516c7756" +} +` +const testAccUserConfig_auth_caching_sha2_password_hex_odd_length = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "0x244124303035246c4f1e0d5d1631594f5c56701f3d327d073a724c706273307a5965516c775" +} +` +const testAccUserConfig_auth_both_string_fields = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hashed = "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19" + auth_string_hex = "0x244124303035246c4f1e0d5d1631594f5c56701f3d327d073a724c706273307a5965516c7756" +} +` + const testAccUserConfig_basic_retain_old_password = ` resource "mysql_user" "test" { user = "jdoe" From 813a3d89f13e6431d739289d207b878dcbe7cd2f Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Sat, 21 Jun 2025 00:13:35 +0200 Subject: [PATCH 5/7] fix validate configuration --- mysql/resource_user_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index fdf93f4f..dc145f5d 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -174,7 +174,7 @@ func TestAccUser_auth_mysql8_validation(t *testing.T) { }, { Config: testAccUserConfig_auth_both_string_fields, - ExpectError: regexp.MustCompile(`can not specify both auth_string_hashed and auth_string_hex`), + ExpectError: regexp.MustCompile(`"auth_string_hex": conflicts with auth_string_hashed`), }, }, }) From 04e2ebd1f13897aeb2ad688b3def5ccb4e4205db Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Sat, 21 Jun 2025 02:01:00 +0200 Subject: [PATCH 6/7] feat: normalize state to proper hex - check hex passes --- mysql/resource_user.go | 11 ++++++++- mysql/resource_user_test.go | 49 ++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index f27076bb..64405be9 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -99,6 +99,7 @@ func resourceUser() *schema.Resource { Type: schema.TypeString, Optional: true, Sensitive: true, + StateFunc: NormalizeHexStringStateFunc, DiffSuppressFunc: SuppressHexStringDiff, ConflictsWith: []string{"plaintext_password", "password", "auth_string_hashed"}, }, @@ -595,7 +596,6 @@ func SuppressHexStringDiff(k, old, new string, d *schema.ResourceData) bool { if normalizedOld == normalizedNew { return true } - return false } @@ -617,6 +617,15 @@ func validateHexString(hexStr string) error { return nil } +func NormalizeHexStringStateFunc(val interface{}) string { + if val == nil { + return "" + } + + hexStr := val.(string) + return normalizeHexString(hexStr) // Always store normalized format +} + // Add this helper function to normalize hex strings func normalizeHexString(hexStr string) string { if hexStr == "" { diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index dc145f5d..b1d9edbd 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -119,22 +119,43 @@ func TestAccUser_auth_mysql8(t *testing.T) { resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), ), }, + }, + }) +} + +func TestAccUser_auth_string_hash_mysql8(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckSkipTiDB(t) + testAccPreCheckSkipMariaDB(t) + testAccPreCheckSkipRds(t) + testAccPreCheckSkipNotMySQLVersionMin(t, "8.0.14") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ { Config: testAccUserConfig_auth_caching_sha2_password_hex_no_prefix, Check: resource.ComposeTestCheckFunc( testAccUserAuthExists("mysql_user.test"), - resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), - resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "hex"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "%"), resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), - resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239"), + ), + }, + { + Config: testAccUserConfig_auth_caching_sha2_password_hex_no_prefix, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("hex", "password"), ), }, { Config: testAccUserConfig_auth_caching_sha2_password_hex_with_prefix, Check: resource.ComposeTestCheckFunc( testAccUserAuthExists("mysql_user.test"), - resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), - resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "hex"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "%"), resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330"), ), @@ -143,8 +164,8 @@ func TestAccUser_auth_mysql8(t *testing.T) { Config: testAccUserConfig_auth_caching_sha2_password_hex_updated, Check: resource.ComposeTestCheckFunc( testAccUserAuthExists("mysql_user.test"), - resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), - resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "hex"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "%"), resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "caching_sha2_password"), resource.TestCheckResourceAttr("mysql_user.test", "auth_string_hex", "0x244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239"), ), @@ -585,24 +606,24 @@ resource "mysql_user" "test" { const testAccUserConfig_auth_caching_sha2_password_hex_no_prefix = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" + user = "hex" + host = "%" auth_plugin = "caching_sha2_password" - auth_string_hex = "244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330" + auth_string_hex = "244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239" } ` const testAccUserConfig_auth_caching_sha2_password_hex_with_prefix = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" + user = "hex" + host = "%" auth_plugin = "caching_sha2_password" auth_string_hex = "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756576B317A5064687A715347765747746B66746A5A4F6E384C41756E6750495330" } ` const testAccUserConfig_auth_caching_sha2_password_hex_updated = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" + user = "hex" + host = "%" auth_plugin = "caching_sha2_password" auth_string_hex = "244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239" } From 6b37bcad90ea951eedf3e823612c909eeac249db Mon Sep 17 00:00:00 2001 From: yogendratamang48 Date: Sat, 21 Jun 2025 02:30:34 +0200 Subject: [PATCH 7/7] feat: add docs --- website/docs/r/user.html.markdown | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index bdf06739..994cf781 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -48,7 +48,6 @@ resource "mysql_user" "nologin" { ``` ## Example Usage with caching_sha2_password Authentication Plugin and plaintext password - ```hcl resource "mysql_user" "nologin" { user = "nologin" @@ -57,6 +56,16 @@ resource "mysql_user" "nologin" { plaintext_password = "password" } ``` +## Example Usage with caching_sha2_password Authentication with Hex Hash + +```hcl +resource "mysql_user" "nologin" { + user = "nologin" + host = "example.com" + auth_plugin = "caching_sha2_password" + auth_string_hex = "0x244124303035246C4F1E0D5D1631594F5C56701F3D327D073A724C706273307A5965516C7756" +} +``` ## Example Usage with AzureAD Authentication Plugin @@ -83,6 +92,7 @@ The following arguments are supported: * `password` - (Optional) Deprecated alias of `plaintext_password`, whose value is _stored as plaintext in state_. Prefer to use `plaintext_password` instead, which stores the password as an unsalted hash. * `auth_plugin` - (Optional) Use an [authentication plugin][ref-auth-plugins] to authenticate the user instead of using password authentication. Description of the fields allowed in the block below. * `auth_string_hashed` - (Optional) Use an already hashed string as a parameter to `auth_plugin`. This can be used with passwords as well as with other auth strings. +* `auth_string_hex` - (Optional) The authentication string as a hexadecimal value(can be with or without `0x` prefix). Primarily used with `caching_sha2_password` authentication plugin. Cannot be used with `plaintext_password`, `password`, or `auth_string_hashed`. * `aad_identity` - (Optional) Required when `auth_plugin` is `aad_auth`. This should be block containing `type` and `identity`. `type` can be one of `user`, `group` and `service_principal`. `identity` then should containt either UPN of user, name of group or Client ID of service principal. * `retain_old_password` - (Optional) When `true`, the old password is retained when changing the password. Defaults to `false`. This use MySQL Dual Password Support feature and requires MySQL version 8.0.14 or newer. See [MySQL Dual Password documentation](https://dev.mysql.com/doc/refman/8.0/en/password-management.html#dual-passwords) for more. * `discard_old_password` - (Optional) When `true`, the old password is deleted. Defaults to `false`. This use MySQL Dual Password Support feature and requires MySQL version 8.0.14 or newer. See [MySQL Dual Password documentation](https://dev.mysql.com/doc/refman/8.0/en/password-management.html#dual-passwords) for more.