Skip to content

feat: support for auth_string_hax #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 133 additions & 17 deletions mysql/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,14 @@ func resourceUser() *schema.Resource {
DiffSuppressFunc: NewEmptyStringSuppressFunc,
ConflictsWith: []string{"plaintext_password", "password"},
},

"auth_string_hex": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
StateFunc: NormalizeHexStringStateFunc,
DiffSuppressFunc: SuppressHexStringDiff,
ConflictsWith: []string{"plaintext_password", "password", "auth_string_hashed"},
},
"tls_option": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -171,6 +178,28 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d
authStm = fmt.Sprintf("%s AS ?", authStm)
}
}
var hashedHex string
if v, ok := d.GetOk("auth_string_hex"); ok {
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)
}
normalizedHex := normalizeHexString(hashedHex)
hexDigits := normalizedHex[2:] // Remove the "0x" prefix for validation

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, hexDigits)
}

}
user := d.Get("user").(string)
host := d.Get("host").(string)

var stmtSQL string
var args []interface{}
Expand All @@ -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
Expand All @@ -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")
}

Expand All @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -286,14 +315,23 @@ 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) != "" {
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"), hexDigits)
}
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,
Expand Down Expand Up @@ -385,10 +423,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 {
Expand All @@ -397,17 +440,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
Expand Down Expand Up @@ -444,7 +487,20 @@ 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]
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)
d.Set("auth_string_hex", "")
} else {
d.Set("auth_string_hashed", "")
d.Set("auth_string_hex", "")
}
}
return nil
}
Expand Down Expand Up @@ -527,3 +583,63 @@ 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 {
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
}

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 == "" {
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
}
128 changes: 128 additions & 0 deletions mysql/resource_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,84 @@ func TestAccUser_auth_mysql8(t *testing.T) {
})
}

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", "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"),
),
},
{
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", "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"),
),
},
{
Config: testAccUserConfig_auth_caching_sha2_password_hex_updated,
Check: resource.ComposeTestCheckFunc(
testAccUserAuthExists("mysql_user.test"),
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"),
),
},
},
})
}

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(`"auth_string_hex": conflicts with auth_string_hashed`),
},
},
})
}

func TestAccUser_authConnect(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
Expand Down Expand Up @@ -526,6 +604,56 @@ resource "mysql_user" "test" {
}
`

const testAccUserConfig_auth_caching_sha2_password_hex_no_prefix = `
resource "mysql_user" "test" {
user = "hex"
host = "%"
auth_plugin = "caching_sha2_password"
auth_string_hex = "244124303035242931790D223576077A1446190832544A61301A256D5245316662534E56317A434A6A625139555A5642486F4B7A6F675266656B583330744379783134313239"
}
`
const testAccUserConfig_auth_caching_sha2_password_hex_with_prefix = `
resource "mysql_user" "test" {
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 = "hex"
host = "%"
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"
Expand Down
Loading