Skip to content

Commit

Permalink
*: support password validation options and variables (#38953)
Browse files Browse the repository at this point in the history
ref #38924, close #38928
  • Loading branch information
CbcWestwolf authored Nov 24, 2022
1 parent d100e93 commit e205f93
Show file tree
Hide file tree
Showing 18 changed files with 766 additions and 26 deletions.
5 changes: 5 additions & 0 deletions errors.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,11 @@ error = '''
SET PASSWORD has no significance for user '%-.48s'@'%-.255s' as authentication plugin does not support it.
'''

["executor:1819"]
error = '''
Your password does not satisfy the current policy requirements
'''

["executor:1827"]
error = '''
The password hash doesn't have the expected format. Check if the correct password algorithm is being used with the PASSWORD() function.
Expand Down
1 change: 1 addition & 0 deletions executor/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ go_library(
"//util/mathutil",
"//util/memory",
"//util/mvmap",
"//util/password-validation",
"//util/pdapi",
"//util/plancodec",
"//util/printer",
Expand Down
19 changes: 12 additions & 7 deletions executor/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,14 @@ func TestSetVar(t *testing.T) {
tk.MustQuery("select @@global.tidb_opt_range_max_size").Check(testkit.Rows("1048576"))
tk.MustExec("set session tidb_opt_range_max_size = 2097152")
tk.MustQuery("select @@session.tidb_opt_range_max_size").Check(testkit.Rows("2097152"))

// test for password validation
tk.MustQuery("SELECT @@GLOBAL.validate_password.enable").Check(testkit.Rows("0"))
tk.MustQuery("SELECT @@GLOBAL.validate_password.length").Check(testkit.Rows("8"))
tk.MustExec("SET GLOBAL validate_password.length = 3")
tk.MustQuery("SELECT @@GLOBAL.validate_password.length").Check(testkit.Rows("4"))
tk.MustExec("SET GLOBAL validate_password.mixed_case_count = 2")
tk.MustQuery("SELECT @@GLOBAL.validate_password.length").Check(testkit.Rows("6"))
}

func TestGetSetNoopVars(t *testing.T) {
Expand Down Expand Up @@ -1407,14 +1415,11 @@ func TestValidateSetVar(t *testing.T) {
tk.MustExec("set @@innodb_lock_wait_timeout = 1073741825")
tk.MustQuery("show warnings").Check(testkit.RowsWithSep("|", "Warning|1292|Truncated incorrect innodb_lock_wait_timeout value: '1073741825'"))

tk.MustExec("set @@global.validate_password_number_count=-1")
tk.MustQuery("show warnings").Check(testkit.RowsWithSep("|", "Warning|1292|Truncated incorrect validate_password_number_count value: '-1'"))

tk.MustExec("set @@global.validate_password_length=-1")
tk.MustQuery("show warnings").Check(testkit.RowsWithSep("|", "Warning|1292|Truncated incorrect validate_password_length value: '-1'"))
tk.MustExec("set @@global.validate_password.number_count=-1")
tk.MustQuery("show warnings").Check(testkit.RowsWithSep("|", "Warning|1292|Truncated incorrect validate_password.number_count value: '-1'"))

tk.MustExec("set @@global.validate_password_length=8")
tk.MustQuery("show warnings").Check(testkit.Rows())
tk.MustExec("set @@global.validate_password.length=-1")
tk.MustQuery("show warnings").Check(testkit.RowsWithSep("|", "Warning|1292|Truncated incorrect validate_password.length value: '-1'"))

err = tk.ExecToErr("set @@tx_isolation=''")
require.True(t, terror.ErrorEqual(err, variable.ErrWrongValueForVar), fmt.Sprintf("err %v", err))
Expand Down
50 changes: 44 additions & 6 deletions executor/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
"github.com/pingcap/tidb/util/collate"
"github.com/pingcap/tidb/util/hack"
"github.com/pingcap/tidb/util/logutil"
pwdValidator "github.com/pingcap/tidb/util/password-validation"
"github.com/pingcap/tidb/util/sem"
"github.com/pingcap/tidb/util/sqlexec"
"github.com/pingcap/tidb/util/timeutil"
Expand Down Expand Up @@ -783,6 +784,23 @@ func (e *SimpleExec) executeRollback(s *ast.RollbackStmt) error {
return nil
}

func (e *SimpleExec) authUsingCleartextPwd(authOpt *ast.AuthOption, authPlugin string) bool {
if authOpt == nil || !authOpt.ByAuthString {
return false
}
return authPlugin == mysql.AuthNativePassword ||
authPlugin == mysql.AuthTiDBSM3Password ||
authPlugin == mysql.AuthCachingSha2Password
}

func (e *SimpleExec) isValidatePasswordEnabled() bool {
validatePwdEnable, err := e.ctx.GetSessionVars().GlobalVarsAccessor.GetGlobalSysVar(variable.ValidatePasswordEnable)
if err != nil {
return false
}
return variable.TiDBOptOn(validatePwdEnable)
}

func (e *SimpleExec) executeCreateUser(ctx context.Context, s *ast.CreateUserStmt) error {
internalCtx := kv.WithInternalSourceType(context.Background(), kv.InternalTxnPrivilege)
// Check `CREATE USER` privilege.
Expand Down Expand Up @@ -874,15 +892,25 @@ func (e *SimpleExec) executeCreateUser(ctx context.Context, s *ast.CreateUserStm
e.ctx.GetSessionVars().StmtCtx.AppendNote(err)
continue
}
authPlugin := mysql.AuthNativePassword
if spec.AuthOpt != nil && spec.AuthOpt.AuthPlugin != "" {
authPlugin = spec.AuthOpt.AuthPlugin
}
if e.isValidatePasswordEnabled() && !s.IsCreateRole {
if spec.AuthOpt == nil || !spec.AuthOpt.ByAuthString && spec.AuthOpt.HashString == "" {
return variable.ErrNotValidPassword.GenWithStackByArgs()
}
if e.authUsingCleartextPwd(spec.AuthOpt, authPlugin) {
if err := pwdValidator.ValidatePassword(e.ctx.GetSessionVars(), spec.AuthOpt.AuthString); err != nil {
return err
}
}
}
pwd, ok := spec.EncodedPassword()

if !ok {
return errors.Trace(ErrPasswordFormat)
}
authPlugin := mysql.AuthNativePassword
if spec.AuthOpt != nil && spec.AuthOpt.AuthPlugin != "" {
authPlugin = spec.AuthOpt.AuthPlugin
}

switch authPlugin {
case mysql.AuthNativePassword, mysql.AuthCachingSha2Password, mysql.AuthTiDBSM3Password, mysql.AuthSocket, mysql.AuthTiDBAuthToken:
Expand Down Expand Up @@ -1071,11 +1099,11 @@ func (e *SimpleExec) executeAlterUser(ctx context.Context, s *ast.AlterUserStmt)
var fields []alterField
if spec.AuthOpt != nil {
if spec.AuthOpt.AuthPlugin == "" {
authplugin, err := e.userAuthPlugin(spec.User.Username, spec.User.Hostname)
curAuthplugin, err := e.userAuthPlugin(spec.User.Username, spec.User.Hostname)
if err != nil {
return err
}
spec.AuthOpt.AuthPlugin = authplugin
spec.AuthOpt.AuthPlugin = curAuthplugin
}
switch spec.AuthOpt.AuthPlugin {
case mysql.AuthNativePassword, mysql.AuthCachingSha2Password, mysql.AuthTiDBSM3Password, mysql.AuthSocket, "":
Expand All @@ -1087,6 +1115,11 @@ func (e *SimpleExec) executeAlterUser(ctx context.Context, s *ast.AlterUserStmt)
default:
return ErrPluginIsNotLoaded.GenWithStackByArgs(spec.AuthOpt.AuthPlugin)
}
if e.isValidatePasswordEnabled() && e.authUsingCleartextPwd(spec.AuthOpt, spec.AuthOpt.AuthPlugin) {
if err := pwdValidator.ValidatePassword(e.ctx.GetSessionVars(), spec.AuthOpt.AuthString); err != nil {
return err
}
}
pwd, ok := spec.EncodedPassword()
if !ok {
return errors.Trace(ErrPasswordFormat)
Expand Down Expand Up @@ -1603,6 +1636,11 @@ func (e *SimpleExec) executeSetPwd(ctx context.Context, s *ast.SetPwdStmt) error
if err != nil {
return err
}
if e.isValidatePasswordEnabled() {
if err := pwdValidator.ValidatePassword(e.ctx.GetSessionVars(), s.Password); err != nil {
return err
}
}
var pwd string
switch authplugin {
case mysql.AuthCachingSha2Password, mysql.AuthTiDBSM3Password:
Expand Down
83 changes: 83 additions & 0 deletions executor/simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,86 @@ func TestUserAttributes(t *testing.T) {
rootTK.MustExec("alter user usr1 comment 'comment1'")
rootTK.MustQuery("select user_attributes from mysql.user where user = 'usr1'").Check(testkit.Rows(`{"metadata": {"comment": "comment1"}}`))
}

func TestValidatePassword(t *testing.T) {
store, _ := testkit.CreateMockStoreAndDomain(t)
tk := testkit.NewTestKit(t, store)
subtk := testkit.NewTestKit(t, store)
err := tk.Session().Auth(&auth.UserIdentity{Username: "root", Hostname: "%"}, nil, nil)
require.NoError(t, err)
tk.MustExec("CREATE USER ''@'localhost'")
tk.MustExec("GRANT ALL PRIVILEGES ON mysql.* TO ''@'localhost';")
err = subtk.Session().Auth(&auth.UserIdentity{Hostname: "localhost"}, nil, nil)
require.NoError(t, err)

authPlugins := []string{mysql.AuthNativePassword, mysql.AuthCachingSha2Password, mysql.AuthTiDBSM3Password}
tk.MustQuery("SELECT @@global.validate_password.enable").Check(testkit.Rows("0"))
tk.MustExec("SET GLOBAL validate_password.enable = 1")
tk.MustQuery("SELECT @@global.validate_password.enable").Check(testkit.Rows("1"))

for _, authPlugin := range authPlugins {
tk.MustExec("DROP USER IF EXISTS testuser")
tk.MustExec(fmt.Sprintf("CREATE USER testuser IDENTIFIED WITH %s BY '!Abc12345678'", authPlugin))

tk.MustExec("SET GLOBAL validate_password.policy = 'LOW'")
// check user name
tk.MustQuery("SELECT @@global.validate_password.check_user_name").Check(testkit.Rows("1"))
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!Abcdroot1234'", "Password Contains User Name")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!Abcdtoor1234'", "Password Contains Reversed User Name")
tk.MustExec("SET PASSWORD FOR 'testuser' = 'testuser'") // password the same as the user name, but run by root
tk.MustExec("ALTER USER testuser IDENTIFIED BY 'testuser'")
tk.MustExec("SET GLOBAL validate_password.check_user_name = 0")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abcdroot1234'")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abcdtoor1234'")
tk.MustExec("SET GLOBAL validate_password.check_user_name = 1")

// LOW: Length
tk.MustExec("SET GLOBAL validate_password.length = 8")
tk.MustQuery("SELECT @@global.validate_password.length").Check(testkit.Rows("8"))
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '1234567'", "Require Password Length: 8")
tk.MustExec("SET GLOBAL validate_password.length = 12")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!Abcdefg123'", "Require Password Length: 12")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abcdefg1234'")
tk.MustExec("SET GLOBAL validate_password.length = 8")

// MEDIUM: Length; numeric, lowercase/uppercase, and special characters
tk.MustExec("SET GLOBAL validate_password.policy = 'MEDIUM'")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abc1234567'")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!ABC1234567'", "Require Password Lowercase Count: 1")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!abc1234567'", "Require Password Uppercase Count: 1")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!ABCDabcd'", "Require Password Digit Count: 1")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY 'Abc1234567'", "Require Password Non-alphanumeric Count: 1")
tk.MustExec("SET GLOBAL validate_password.special_char_count = 0")
tk.MustExec("ALTER USER testuser IDENTIFIED BY 'Abc1234567'")
tk.MustExec("SET GLOBAL validate_password.special_char_count = 1")
tk.MustExec("SET GLOBAL validate_password.length = 3")
tk.MustQuery("SELECT @@GLOBAL.validate_password.length").Check(testkit.Rows("4"))

// STRONG: Length; numeric, lowercase/uppercase, and special characters; dictionary file
tk.MustExec("SET GLOBAL validate_password.policy = 'STRONG'")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abc1234567'")
tk.MustExec(fmt.Sprintf("SET GLOBAL validate_password.dictionary = '%s'", "1234;5678"))
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abc123567'")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abc43218765'")
tk.MustContainErrMsg("ALTER USER testuser IDENTIFIED BY '!Abc1234567'", "Password contains word in the dictionary")
tk.MustExec("SET GLOBAL validate_password.dictionary = ''")
tk.MustExec("ALTER USER testuser IDENTIFIED BY '!Abc1234567'")

// "IDENTIFIED AS 'xxx'" is not affected by validation
tk.MustExec(fmt.Sprintf("ALTER USER testuser IDENTIFIED WITH '%s' AS ''", authPlugin))
}
tk.MustContainErrMsg("CREATE USER 'testuser1'@'localhost'", "Your password does not satisfy the current policy requirements")
tk.MustContainErrMsg("CREATE USER 'testuser1'@'localhost' IDENTIFIED WITH 'caching_sha2_password'", "Your password does not satisfy the current policy requirements")
tk.MustContainErrMsg("CREATE USER 'testuser1'@'localhost' IDENTIFIED WITH 'caching_sha2_password' AS ''", "Your password does not satisfy the current policy requirements")

// if the username is '', all password can pass the check_user_name
subtk.MustQuery("SELECT user(), current_user()").Check(testkit.Rows("@localhost @localhost"))
subtk.MustQuery("SELECT @@global.validate_password.check_user_name").Check(testkit.Rows("1"))
subtk.MustQuery("SELECT @@global.validate_password.enable").Check(testkit.Rows("1"))
subtk.MustExec("ALTER USER ''@'localhost' IDENTIFIED BY ''")
subtk.MustExec("ALTER USER ''@'localhost' IDENTIFIED BY 'abcd'")

// CREATE ROLE is not affected by password validation
tk.MustExec("SET GLOBAL validate_password.enable = 1")
tk.MustExec("CREATE ROLE role1")
}
1 change: 1 addition & 0 deletions expression/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ go_library(
"//util/mathutil",
"//util/mock",
"//util/parser",
"//util/password-validation",
"//util/plancodec",
"//util/printer",
"//util/sem",
Expand Down
66 changes: 64 additions & 2 deletions expression/builtin_encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/pingcap/tidb/types"
"github.com/pingcap/tidb/util/chunk"
"github.com/pingcap/tidb/util/encrypt"
pwdValidator "github.com/pingcap/tidb/util/password-validation"
"github.com/pingcap/tipb/go-tipb"
)

Expand Down Expand Up @@ -73,6 +74,7 @@ var (
_ builtinFunc = &builtinSHA2Sig{}
_ builtinFunc = &builtinUncompressSig{}
_ builtinFunc = &builtinUncompressedLengthSig{}
_ builtinFunc = &builtinValidatePasswordStrengthSig{}
)

// aesModeAttr indicates that the key length and iv attribute for specific block_encryption_mode.
Expand Down Expand Up @@ -728,7 +730,6 @@ func (c *sm3FunctionClass) getFunction(ctx sessionctx.Context, args []Expression
bf.tp.SetCollate(collate)
bf.tp.SetFlen(40)
sig := &builtinSM3Sig{bf}
//sig.setPbCode(tipb.ScalarFuncSig_SM3) // TODO
return sig, nil
}

Expand Down Expand Up @@ -1010,5 +1011,66 @@ type validatePasswordStrengthFunctionClass struct {
}

func (c *validatePasswordStrengthFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
return nil, errFunctionNotExists.GenWithStackByArgs("FUNCTION", "VALIDATE_PASSWORD_STRENGTH")
if err := c.verifyArgs(args); err != nil {
return nil, err
}
bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETInt, types.ETString)
if err != nil {
return nil, err
}
bf.tp.SetFlen(21)
sig := &builtinValidatePasswordStrengthSig{bf}
return sig, nil
}

type builtinValidatePasswordStrengthSig struct {
baseBuiltinFunc
}

func (b *builtinValidatePasswordStrengthSig) Clone() builtinFunc {
newSig := &builtinValidatePasswordStrengthSig{}
newSig.cloneFrom(&b.baseBuiltinFunc)
return newSig
}

// evalInt evals VALIDATE_PASSWORD_STRENGTH(str).
// See https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html#function_validate-password-strength
func (b *builtinValidatePasswordStrengthSig) evalInt(row chunk.Row) (int64, bool, error) {
globalVars := b.ctx.GetSessionVars().GlobalVarsAccessor
str, isNull, err := b.args[0].EvalString(b.ctx, row)
if err != nil || isNull {
return 0, true, err
} else if len([]rune(str)) < 4 {
return 0, false, nil
}
if validation, err := globalVars.GetGlobalSysVar(variable.ValidatePasswordEnable); err != nil {
return 0, true, err
} else if !variable.TiDBOptOn(validation) {
return 0, false, nil
}
return b.validateStr(str, &globalVars)
}

func (b *builtinValidatePasswordStrengthSig) validateStr(str string, globalVars *variable.GlobalVarAccessor) (int64, bool, error) {
if warn, err := pwdValidator.ValidateUserNameInPassword(str, b.ctx.GetSessionVars()); err != nil {
return 0, true, err
} else if len(warn) > 0 {
return 0, false, nil
}
if warn, err := pwdValidator.ValidatePasswordLowPolicy(str, globalVars); err != nil {
return 0, true, err
} else if len(warn) > 0 {
return 25, false, nil
}
if warn, err := pwdValidator.ValidatePasswordMediumPolicy(str, globalVars); err != nil {
return 0, true, err
} else if len(warn) > 0 {
return 50, false, nil
}
if ok, err := pwdValidator.ValidateDictionaryPassword(str, globalVars); err != nil {
return 0, true, err
} else if !ok {
return 75, false, nil
}
return 100, false, nil
}
Loading

0 comments on commit e205f93

Please sign in to comment.