Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1c659b0
*: support MySQL dual passwords (RETAIN CURRENT PASSWORD / DISCARD OL…
takaidohigasi Apr 24, 2026
99aa1d2
*: correct MySQL error codes and messages for dual-password clauses
takaidohigasi Apr 24, 2026
a591869
*: address CodeRabbit review — plugin-empty, host-filter, helper, com…
takaidohigasi Apr 24, 2026
cca3079
privilege: degrade gracefully when the retained secondary hash is cor…
takaidohigasi Apr 24, 2026
a7f123c
parser: bump TestKeywordsLength count for OLD + RETAIN
takaidohigasi Apr 25, 2026
c6b8618
executor: defer APPLICATION_PASSWORD_ADMIN lookup until RETAIN/DISCAR…
takaidohigasi Apr 28, 2026
3376e60
*: regenerate errors.toml for new dual-password error codes
takaidohigasi Apr 28, 2026
479f24b
tests: record APPLICATION_PASSWORD_ADMIN in SHOW PRIVILEGES output
takaidohigasi Apr 28, 2026
191345d
privilege: keep Error log lines byte-identical with master
takaidohigasi Apr 28, 2026
2ad2933
tests: pin RETAIN/DISCARD + COMMENT atomic update
takaidohigasi May 1, 2026
b5f5a17
executor: align dual-password privilege checks with MySQL
takaidohigasi May 2, 2026
85d5dbd
executor: apply dual-password clauses per user
takaidohigasi May 2, 2026
4963ab8
*: surface dual-password clauses in SecureText / SecurityString and f…
takaidohigasi May 2, 2026
d2e40e4
*: split — revert non-parser changes; behavior moves to follow-up PR
takaidohigasi May 15, 2026
8776699
Merge remote-tracking branch 'origin/master' into feature/dual-password
takaidohigasi May 15, 2026
adb27a7
*: address @D3Hunter review (parser PR)
takaidohigasi May 17, 2026
05a3ff1
parser: parse ALTER USER USER() dual-password (Codex P2 fix)
takaidohigasi May 17, 2026
bf6e1a0
executor: use plannererrors.ErrNotSupportedYet for dual-password stubs
takaidohigasi May 17, 2026
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
21 changes: 21 additions & 0 deletions pkg/executor/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,22 @@ func checkPasswordReusePolicy(ctx context.Context, sqlExecutor sqlexec.SQLExecut
}

func (e *SimpleExec) executeAlterUser(ctx context.Context, s *ast.AlterUserStmt) error {
// MySQL 8.0 dual-password clauses (RETAIN CURRENT PASSWORD /
// DISCARD OLD PASSWORD) are accepted by the parser in this PR but the
// matching executor / privilege / storage logic lands in a follow-up
// (pingcap/tidb#68393). Reject explicitly with a stable error so users
// see "not supported yet" instead of silent success.
//
// Both the named-user form (Specs) and the current-user form
// (CurrentDualPasswordOption on the USER() branch) are caught here.
if s.CurrentDualPasswordOption != nil {
return plannererrors.ErrNotSupportedYet.GenWithStackByArgs("dual password (RETAIN CURRENT PASSWORD / DISCARD OLD PASSWORD)")
}
for _, spec := range s.Specs {
if spec.DualPasswordOption != nil {
return plannererrors.ErrNotSupportedYet.GenWithStackByArgs("dual password (RETAIN CURRENT PASSWORD / DISCARD OLD PASSWORD)")
}
}
disableSandBoxMode := false
var err error
if e.Ctx().InSandBoxMode() {
Expand Down Expand Up @@ -2493,6 +2509,11 @@ func userExistsInternal(ctx context.Context, sqlExecutor sqlexec.SQLExecutor, na
}

func (e *SimpleExec) executeSetPwd(ctx context.Context, s *ast.SetPwdStmt) error {
// See executeAlterUser: RETAIN CURRENT PASSWORD parsing lands in this PR;
// execution lands in pingcap/tidb#68393. Until then, fail closed.
if s.RetainCurrentPassword {
return plannererrors.ErrNotSupportedYet.GenWithStackByArgs("SET PASSWORD ... RETAIN CURRENT PASSWORD")
}
ctx = kv.WithInternalSourceType(ctx, kv.InternalTxnPrivilege)
sysSession, err := e.GetSysSession()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/executor/test/passwordtest/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ go_test(
"password_management_test.go",
],
flaky = True,
shard_count = 9,
shard_count = 10,
deps = [
"//pkg/domain",
"//pkg/errno",
Expand Down
38 changes: 38 additions & 0 deletions pkg/executor/test/passwordtest/password_management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,44 @@ func checkAuthUser(t *testing.T, tk *testkit.TestKit, user string, failedLoginCo
require.Equal(t, autoAccountLocked, ua[0].PasswordLocking.AutoAccountLocked)
}

// TestDualPasswordParserOnlyStub guards the executor stub added in the
// parser-only PR (#68028). The grammar now accepts MySQL 8.0 dual-password
// clauses (RETAIN CURRENT PASSWORD / DISCARD OLD PASSWORD), but the matching
// executor / privilege / storage logic lands in the follow-up PR (#68393).
// Until then the executor must fail fast with ER_NOT_SUPPORTED_YET so users
// don't see silent success.
//
// When #68393 lands and removes the stubs, this test should be replaced by
// the real dual-password coverage that lives there.
func TestDualPasswordParserOnlyStub(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
require.NoError(t, tk.Session().Auth(&auth.UserIdentity{Username: "root", Hostname: "%"}, nil, nil, nil))

tk.MustExec("DROP USER IF EXISTS dpstub")
tk.MustExec("CREATE USER dpstub IDENTIFIED BY 'old'")

// ALTER USER ... RETAIN CURRENT PASSWORD must fail with ER_NOT_SUPPORTED_YET.
tk.MustGetErrCode("ALTER USER dpstub IDENTIFIED BY 'new' RETAIN CURRENT PASSWORD", errno.ErrNotSupportedYet)
// ALTER USER ... DISCARD OLD PASSWORD must fail with ER_NOT_SUPPORTED_YET.
tk.MustGetErrCode("ALTER USER dpstub DISCARD OLD PASSWORD", errno.ErrNotSupportedYet)
// SET PASSWORD ... RETAIN CURRENT PASSWORD must fail with ER_NOT_SUPPORTED_YET.
tk.MustGetErrCode("SET PASSWORD FOR dpstub = 'new' RETAIN CURRENT PASSWORD", errno.ErrNotSupportedYet)

// Current-user form: MySQL 8.0 accepts dual-password on the USER() branch.
// The stub propagates CurrentDualPasswordOption to the synthetic UserSpec
// and also fails with ER_NOT_SUPPORTED_YET. Authenticate as dpstub first
// so USER() resolves to dpstub@%.
subTK := testkit.NewTestKit(t, store)
require.NoError(t, subTK.Session().Auth(&auth.UserIdentity{Username: "dpstub", Hostname: "%"}, sha1Password("old"), nil, nil))
subTK.MustGetErrCode("ALTER USER USER() IDENTIFIED BY 'p3' RETAIN CURRENT PASSWORD", errno.ErrNotSupportedYet)
subTK.MustGetErrCode("ALTER USER USER() DISCARD OLD PASSWORD", errno.ErrNotSupportedYet)

// A regular ALTER USER (no dual-password clause) must still succeed —
// the stub guard only triggers when DualPasswordOption is set.
tk.MustExec("ALTER USER dpstub IDENTIFIED BY 'plain'")
}

func selectSQL(user string) string {
userAttributesSQL := new(strings.Builder)
sqlescape.MustFormatSQL(userAttributesSQL, "SELECT user_attributes from mysql.user WHERE USER = %? AND HOST = 'localhost' for update", user)
Expand Down
112 changes: 97 additions & 15 deletions pkg/parser/ast/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1405,8 +1405,9 @@ func (n *SetCharsetStmt) Accept(v Visitor) (Node, bool) {
type SetPwdStmt struct {
stmtNode

User *auth.UserIdentity
Password string
User *auth.UserIdentity
Password string
RetainCurrentPassword bool
}

// Restore implements Node interface.
Expand All @@ -1420,11 +1421,17 @@ func (n *SetPwdStmt) Restore(ctx *format.RestoreCtx) error {
}
ctx.WritePlain("=")
ctx.WriteString(n.Password)
if n.RetainCurrentPassword {
ctx.WriteKeyWord(" RETAIN CURRENT PASSWORD")
}
return nil
}

// SecureText implements SensitiveStatement interface.
func (n *SetPwdStmt) SecureText() string {
if n.RetainCurrentPassword {
return fmt.Sprintf("set password for user %s RETAIN CURRENT PASSWORD", n.User)
}
return fmt.Sprintf("set password for user %s", n.User)
}

Expand Down Expand Up @@ -1545,9 +1552,10 @@ func (n *SetDefaultRoleStmt) Accept(v Visitor) (Node, bool) {

Comment thread
D3Hunter marked this conversation as resolved.
// UserSpec is used for parsing create user statement.
type UserSpec struct {
User *auth.UserIdentity
AuthOpt *AuthOption
IsRole bool
User *auth.UserIdentity
AuthOpt *AuthOption
DualPasswordOption *DualPasswordOption
IsRole bool
}

// Restore implements Node interface.
Expand All @@ -1561,19 +1569,40 @@ func (n *UserSpec) Restore(ctx *format.RestoreCtx) error {
return errors.Annotate(err, "An error occurred while restore UserSpec.AuthOpt")
}
}
if n.DualPasswordOption != nil {
ctx.WritePlain(" ")
if err := n.DualPasswordOption.Restore(ctx); err != nil {
return errors.Annotate(err, "An error occurred while restore UserSpec.DualPasswordOption")
}
}
return nil
}

// SecurityString formats the UserSpec without password information.
// SecurityString formats the UserSpec without password information. The
// dual-password clause (RETAIN CURRENT PASSWORD / DISCARD OLD PASSWORD) is
// non-secret and is surfaced verbatim so the redacted output preserves the
// fact that the statement targets the secondary-password slot.
func (n *UserSpec) SecurityString() string {
withPassword := false
if opt := n.AuthOpt; opt != nil {
if len(opt.AuthString) > 0 || len(opt.HashString) > 0 {
withPassword = true
}
}
dualClause := ""
if n.DualPasswordOption != nil {
switch n.DualPasswordOption.Type {
case DualPasswordRetainCurrent:
dualClause = " RETAIN CURRENT PASSWORD"
case DualPasswordDiscardOld:
dualClause = " DISCARD OLD PASSWORD"
}
}
if withPassword {
return fmt.Sprintf("{%s password = ***}", n.User)
return fmt.Sprintf("{%s password = ***%s}", n.User, dualClause)
}
if dualClause != "" {
return fmt.Sprintf("{%s%s}", n.User, dualClause)
}
return n.User.String()
}
Expand Down Expand Up @@ -1698,6 +1727,40 @@ const (
UserResourceGroupName
)

// DualPasswordOptionType identifies the dual-password action attached to a
// UserSpec by the parser. Dedicated to dual-password semantics so the AST
// does not conflate it with PasswordOrLockOption (account lock, expiration,
// failed-login policy, etc.) which has different per-statement scoping rules.
type DualPasswordOptionType int

const (
// DualPasswordRetainCurrent corresponds to RETAIN CURRENT PASSWORD.
DualPasswordRetainCurrent DualPasswordOptionType = iota + 1
// DualPasswordDiscardOld corresponds to DISCARD OLD PASSWORD.
DualPasswordDiscardOld
)

// DualPasswordOption represents a per-UserSpec MySQL 8.0 dual-password clause
// (RETAIN CURRENT PASSWORD or DISCARD OLD PASSWORD). The grammar attaches it
// to the UserSpec rather than to AlterUserStmt because MySQL allows different
// dual-password actions per spec inside a multi-user ALTER USER statement.
type DualPasswordOption struct {
Type DualPasswordOptionType
}

// Restore implements Node interface.
func (d *DualPasswordOption) Restore(ctx *format.RestoreCtx) error {
switch d.Type {
case DualPasswordRetainCurrent:
ctx.WriteKeyWord("RETAIN CURRENT PASSWORD")
case DualPasswordDiscardOld:
ctx.WriteKeyWord("DISCARD OLD PASSWORD")
default:
return errors.Errorf("Unsupported DualPasswordOption.Type %d", d.Type)
}
return nil
}

type PasswordOrLockOption struct {
Type int
Count int64
Expand Down Expand Up @@ -1876,14 +1939,20 @@ func (n *CreateUserStmt) SecureText() string {
type AlterUserStmt struct {
stmtNode

IfExists bool
CurrentAuth *AuthOption
Specs []*UserSpec
AuthTokenOrTLSOptions []*AuthTokenOrTLSOption
ResourceOptions []*ResourceOption
PasswordOrLockOptions []*PasswordOrLockOption
CommentOrAttributeOption *CommentOrAttributeOption
ResourceGroupNameOption *ResourceGroupNameOption
IfExists bool
CurrentAuth *AuthOption
// CurrentDualPasswordOption carries the dual-password clause attached to the
// `ALTER USER USER() ...` (current-user) form. The named-user form stores
// its dual-password clause on the per-UserSpec DualPasswordOption instead.
// The executor must propagate this into the synthetic UserSpec it builds
// from CurrentAuth so downstream code paths only need to inspect Specs.
CurrentDualPasswordOption *DualPasswordOption
Specs []*UserSpec
AuthTokenOrTLSOptions []*AuthTokenOrTLSOption
ResourceOptions []*ResourceOption
PasswordOrLockOptions []*PasswordOrLockOption
CommentOrAttributeOption *CommentOrAttributeOption
ResourceGroupNameOption *ResourceGroupNameOption
}

// Restore implements Node interface.
Expand All @@ -1898,6 +1967,19 @@ func (n *AlterUserStmt) Restore(ctx *format.RestoreCtx) error {
if err := n.CurrentAuth.Restore(ctx); err != nil {
return errors.Annotate(err, "An error occurred while restore AlterUserStmt.CurrentAuth")
}
if n.CurrentDualPasswordOption != nil {
ctx.WritePlain(" ")
if err := n.CurrentDualPasswordOption.Restore(ctx); err != nil {
return errors.Annotate(err, "An error occurred while restore AlterUserStmt.CurrentDualPasswordOption")
}
}
} else if n.CurrentDualPasswordOption != nil {
// Standalone DISCARD OLD PASSWORD on the current-user form (no IDENTIFIED BY).
ctx.WriteKeyWord("USER")
ctx.WritePlain("() ")
if err := n.CurrentDualPasswordOption.Restore(ctx); err != nil {
return errors.Annotate(err, "An error occurred while restore AlterUserStmt.CurrentDualPasswordOption")
}
}
for i, v := range n.Specs {
if i != 0 {
Expand Down
2 changes: 2 additions & 0 deletions pkg/parser/keywords.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pkg/parser/keywords_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ func TestKeywords(t *testing.T) {
}

func TestKeywordsLength(t *testing.T) {
require.Equal(t, 679, len(parser.Keywords))
// 681 = 679 baseline + 2 (OLD, RETAIN) added by the dual-password feature
// (both non-reserved, so the reserved count is unchanged).
require.Equal(t, 681, len(parser.Keywords))

reservedNr := 0
for _, kw := range parser.Keywords {
Expand Down
2 changes: 2 additions & 0 deletions pkg/parser/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ var tokenMap = map[string]int{
"OF": of,
"OFF": off,
"OFFSET": offset,
"OLD": old,
"OLTP_READ_ONLY": oltpReadOnly,
"OLTP_READ_WRITE": oltpReadWrite,
"OLTP_WRITE_ONLY": oltpWriteOnly,
Expand Down Expand Up @@ -709,6 +710,7 @@ var tokenMap = map[string]int{
"RESTORES": restores,
"RESTORED_TS": restoredTS,
"RESTRICT": restrict,
"RETAIN": retain,
"REVERSE": reverse,
"REVOKE": revoke,
"RIGHT": right,
Expand Down
Loading