You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
*: support MySQL dual passwords (RETAIN CURRENT PASSWORD / DISCARD OLD PASSWORD)
Adds MySQL 8.0-compatible dual-password support for credential rotation
without downtime:
- Parser: new RETAIN CURRENT PASSWORD and DISCARD OLD PASSWORD clauses
on ALTER USER and SET PASSWORD; RETAIN/DISCARD/OLD registered as
non-reserved keywords.
- Privilege cache: UserRecord.AdditionalAuthenticationString, decoded
from mysql.user.user_attributes '$.additional_password'.
- Auth: ConnectionVerification falls back to the secondary password for
mysql_native_password / caching_sha2_password / tidb_sm3_password when
the primary check fails.
- Privilege: APPLICATION_PASSWORD_ADMIN dynamic privilege; required when
RETAIN/DISCARD is applied to another user's account (self-service is
always allowed, whether the statement uses CURRENT_USER() or names
the caller explicitly).
- Executor: executeCreateUser rejects RETAIN/DISCARD (per MySQL);
executeAlterUser and executeSetPwd promote the current
authentication_string to user_attributes.additional_password on
RETAIN, JSON_REMOVE on DISCARD, and silently drop the secondary on a
plugin change (matching MySQL behavior).
- Error codes: ErrCurrentPasswordCannotBeRetainedPluginChange (4058),
ErrCurrentPasswordCannotBeRetainedEmptyNew (4059) — MySQL-compatible.
- Tests: 10 unit tests in pkg/executor/test/passwordtest/dual_password_test.go
plus integration test in tests/integrationtest/t/executor/dual_password.test.
Ref #60587
Copy file name to clipboardExpand all lines: pkg/errno/errname.go
+6Lines changed: 6 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -946,6 +946,12 @@ var MySQLErrName = map[uint16]*mysql.ErrMessage{
946
946
ErrDependentByCheckConstraint: mysql.Message("Check constraint '%s' uses column '%s', hence column cannot be dropped or renamed.", nil),
947
947
ErrEngineAttributeNotSupported: mysql.Message("Storage engine does not support ENGINE_ATTRIBUTE.", nil),
948
948
ErrJSONInBooleanContext: mysql.Message("Evaluating a JSON value in SQL boolean context does an implicit comparison against JSON integer 0; if this is not what you want, consider converting JSON to a SQL numeric type with JSON_VALUE RETURNING", nil),
949
+
// Dual-password errors — match MySQL 8.0 text.
950
+
ErrCurrentPasswordNotRequired: mysql.Message("Current password needs to be specified only if it exists.", nil),
951
+
ErrDuplicatePasswordSpecifiedKeywords: mysql.Message("RETAIN CURRENT PASSWORD and DISCARD OLD PASSWORD clauses are mutually exclusive in a single ALTER USER statement.", nil),
952
+
ErrCurrentPasswordCannotBeRetainedPluginChange: mysql.Message("Current password can not be retained for user '%-.64s'@'%-.64s' because authentication plugin is being changed.", nil),
953
+
ErrCurrentPasswordCannotBeRetainedEmptyNew: mysql.Message("Current password can not be retained for user '%-.64s'@'%-.64s' because new password is empty.", nil),
954
+
ErrCurrentPasswordCannotBeRetainedEmptyPrimary: mysql.Message("Empty current password can not be retained as secondary password for user '%-.64s'@'%-.64s'.", nil),
949
955
// MariaDB errors.
950
956
ErrOnlyOneDefaultPartionAllowed: mysql.Message("Only one DEFAULT partition allowed", nil),
sqlescape.MustFormatSQL(sql, `SELECT authentication_string FROM %n.%n WHERE User=%? AND Host=%? FOR UPDATE;`, mysql.SystemDB, mysql.UserTable, name, strings.ToLower(host))
sqlescape.MustFormatSQL(sql, `SELECT * FROM %n.%n WHERE User=%? AND Host=%? FOR UPDATE;`, mysql.SystemDB, mysql.UserTable, name, strings.ToLower(host))
@@ -2515,22 +2641,34 @@ func (e *SimpleExec) executeSetPwd(ctx context.Context, s *ast.SetPwdStmt) error
2515
2641
2516
2642
varu, hstring
2517
2643
disableSandboxMode:=false
2518
-
ifs.User==nil||s.User.CurrentUser {
2519
-
ife.Ctx().GetSessionVars().User==nil {
2644
+
sessUser:=e.Ctx().GetSessionVars().User
2645
+
// setPwdForSelf matches executeAlterUser's alterCurrentUser idiom: treat an
2646
+
// explicit `FOR 'self'@'host'` that names the caller as self-service, not
2647
+
// cross-user. MySQL 8.0 documents this — APPLICATION_PASSWORD_ADMIN is only
2648
+
// required when the statement names a *different* account.
@@ -2599,7 +2745,17 @@ func (e *SimpleExec) executeSetPwd(ctx context.Context, s *ast.SetPwdStmt) error
2599
2745
}
2600
2746
// update mysql.user
2601
2747
sql:=new(strings.Builder)
2602
-
sqlescape.MustFormatSQL(sql, `UPDATE %n.%n SET authentication_string=%?,password_expired='N',password_last_changed=current_timestamp() WHERE User=%? AND Host=%?;`, mysql.SystemDB, mysql.UserTable, pwd, u, strings.ToLower(h))
2748
+
ifs.RetainCurrentPassword {
2749
+
// If RETAIN CURRENT PASSWORD is specified, promote the current authentication_string
2750
+
// to user_attributes.$.additional_password as part of this UPDATE.
2751
+
attr, err:=buildAdditionalPasswordJSON(ctx, sqlExecutor, u, h)
2752
+
iferr!=nil {
2753
+
returnerr
2754
+
}
2755
+
sqlescape.MustFormatSQL(sql, `UPDATE %n.%n SET authentication_string=%?,password_expired='N',password_last_changed=current_timestamp(),user_attributes=json_merge_patch(coalesce(user_attributes, '{}'), %?) WHERE User=%? AND Host=%?;`, mysql.SystemDB, mysql.UserTable, pwd, attr, u, strings.ToLower(h))
2756
+
} else {
2757
+
sqlescape.MustFormatSQL(sql, `UPDATE %n.%n SET authentication_string=%?,password_expired='N',password_last_changed=current_timestamp() WHERE User=%? AND Host=%?;`, mysql.SystemDB, mysql.UserTable, pwd, u, strings.ToLower(h))
0 commit comments