* Fix extra space * Fix regular expression * Fix error template name * Simplify check code, fix default values, add test * Fix router tests * Fix fmt * Fix setting and lint * Move cleaning up code to test, improve comments * Tidy up variable declarationtags/v1.21.12.1
| @@ -345,7 +345,8 @@ IMPORT_LOCAL_PATHS = false | |||
| ; Set to true to prevent all users (including admin) from creating custom git hooks | |||
| DISABLE_GIT_HOOKS = false | |||
| ;Comma separated list of character classes required to pass minimum complexity. | |||
| ;If left empty or no valid values are specified, the default values (`lower,upper,digit,spec`) will be used. | |||
| ;If left empty or no valid values are specified, the default values ("lower,upper,digit,spec") will be used. | |||
| ;Use "off" to disable checking. | |||
| PASSWORD_COMPLEXITY = lower,upper,digit,spec | |||
| ; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt" | |||
| PASSWORD_HASH_ALGO = pbkdf2 | |||
| @@ -219,7 +219,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||
| - lower - use one or more lower latin characters | |||
| - upper - use one or more upper latin characters | |||
| - digit - use one or more digits | |||
| - spec - use one or more special characters as ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol. | |||
| - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` | |||
| - off - do not check password complexity | |||
| ## OpenID (`openid`) | |||
| @@ -7,45 +7,60 @@ package password | |||
| import ( | |||
| "crypto/rand" | |||
| "math/big" | |||
| "regexp" | |||
| "strings" | |||
| "sync" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| var matchComplexities = map[string]regexp.Regexp{} | |||
| var matchComplexityOnce sync.Once | |||
| var validChars string | |||
| var validComplexities = map[string]string{ | |||
| "lower": "abcdefghijklmnopqrstuvwxyz", | |||
| "upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | |||
| "digit": "0123456789", | |||
| "spec": `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-", | |||
| } | |||
| var ( | |||
| matchComplexityOnce sync.Once | |||
| validChars string | |||
| requiredChars []string | |||
| charComplexities = map[string]string{ | |||
| "lower": `abcdefghijklmnopqrstuvwxyz`, | |||
| "upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, | |||
| "digit": `0123456789`, | |||
| "spec": ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", | |||
| } | |||
| ) | |||
| // NewComplexity for preparation | |||
| func NewComplexity() { | |||
| matchComplexityOnce.Do(func() { | |||
| if len(setting.PasswordComplexity) > 0 { | |||
| for key, val := range setting.PasswordComplexity { | |||
| matchComplexity := regexp.MustCompile(val) | |||
| matchComplexities[key] = *matchComplexity | |||
| validChars += validComplexities[key] | |||
| setupComplexity(setting.PasswordComplexity) | |||
| }) | |||
| } | |||
| func setupComplexity(values []string) { | |||
| if len(values) != 1 || values[0] != "off" { | |||
| for _, val := range values { | |||
| if chars, ok := charComplexities[val]; ok { | |||
| validChars += chars | |||
| requiredChars = append(requiredChars, chars) | |||
| } | |||
| } else { | |||
| for _, val := range validComplexities { | |||
| validChars += val | |||
| } | |||
| if len(requiredChars) == 0 { | |||
| // No valid character classes found; use all classes as default | |||
| for _, chars := range charComplexities { | |||
| validChars += chars | |||
| requiredChars = append(requiredChars, chars) | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| if validChars == "" { | |||
| // No complexities to check; provide a sensible default for password generation | |||
| validChars = charComplexities["lower"] + charComplexities["upper"] + charComplexities["digit"] | |||
| } | |||
| } | |||
| // IsComplexEnough return True if password is Complexity | |||
| // IsComplexEnough return True if password meets complexity settings | |||
| func IsComplexEnough(pwd string) bool { | |||
| if len(setting.PasswordComplexity) > 0 { | |||
| NewComplexity() | |||
| for _, val := range matchComplexities { | |||
| if !val.MatchString(pwd) { | |||
| NewComplexity() | |||
| if len(validChars) > 0 { | |||
| for _, req := range requiredChars { | |||
| if !strings.ContainsAny(req, pwd) { | |||
| return false | |||
| } | |||
| } | |||
| @@ -0,0 +1,75 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package password | |||
| import ( | |||
| "testing" | |||
| "github.com/stretchr/testify/assert" | |||
| ) | |||
| func TestComplexity_IsComplexEnough(t *testing.T) { | |||
| matchComplexityOnce.Do(func() {}) | |||
| testlist := []struct { | |||
| complexity []string | |||
| truevalues []string | |||
| falsevalues []string | |||
| }{ | |||
| {[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, | |||
| {[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, | |||
| {[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, | |||
| {[]string{"spec"}, []string{"=!$", "abc!"}, []string{"abc", "ABC", "123", ""}}, | |||
| {[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, | |||
| {[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, | |||
| {[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, | |||
| } | |||
| for _, test := range testlist { | |||
| testComplextity(test.complexity) | |||
| for _, val := range test.truevalues { | |||
| assert.True(t, IsComplexEnough(val)) | |||
| } | |||
| for _, val := range test.falsevalues { | |||
| assert.False(t, IsComplexEnough(val)) | |||
| } | |||
| } | |||
| // Remove settings for other tests | |||
| testComplextity([]string{"off"}) | |||
| } | |||
| func TestComplexity_Generate(t *testing.T) { | |||
| matchComplexityOnce.Do(func() {}) | |||
| const maxCount = 50 | |||
| const pwdLen = 50 | |||
| test := func(t *testing.T, modes []string) { | |||
| testComplextity(modes) | |||
| for i := 0; i < maxCount; i++ { | |||
| pwd, err := Generate(pwdLen) | |||
| assert.NoError(t, err) | |||
| assert.Equal(t, pwdLen, len(pwd)) | |||
| assert.True(t, IsComplexEnough(pwd), "Failed complexities with modes %+v for generated: %s", modes, pwd) | |||
| } | |||
| } | |||
| test(t, []string{"lower"}) | |||
| test(t, []string{"upper"}) | |||
| test(t, []string{"lower", "upper", "spec"}) | |||
| test(t, []string{"off"}) | |||
| test(t, []string{""}) | |||
| // Remove settings for other tests | |||
| testComplextity([]string{"off"}) | |||
| } | |||
| func testComplextity(values []string) { | |||
| // Cleanup previous values | |||
| validChars = "" | |||
| requiredChars = make([]string, 0, len(values)) | |||
| setupComplexity(values) | |||
| } | |||
| @@ -149,7 +149,7 @@ var ( | |||
| MinPasswordLength int | |||
| ImportLocalPaths bool | |||
| DisableGitHooks bool | |||
| PasswordComplexity map[string]string | |||
| PasswordComplexity []string | |||
| PasswordHashAlgo string | |||
| // UI settings | |||
| @@ -781,26 +781,14 @@ func NewContext() { | |||
| InternalToken = loadInternalToken(sec) | |||
| var dictPC = map[string]string{ | |||
| "lower": "[a-z]+", | |||
| "upper": "[A-Z]+", | |||
| "digit": "[0-9]+", | |||
| "spec": `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-", | |||
| } | |||
| PasswordComplexity = make(map[string]string) | |||
| cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") | |||
| for _, y := range cfgdata { | |||
| ts := strings.TrimSpace(y) | |||
| for a := range dictPC { | |||
| if strings.ToLower(ts) == a { | |||
| PasswordComplexity[ts] = dictPC[ts] | |||
| break | |||
| } | |||
| PasswordComplexity = make([]string, 0, len(cfgdata)) | |||
| for _, name := range cfgdata { | |||
| name := strings.ToLower(strings.Trim(name, `"`)) | |||
| if name != "" { | |||
| PasswordComplexity = append(PasswordComplexity, name) | |||
| } | |||
| } | |||
| if len(PasswordComplexity) == 0 { | |||
| PasswordComplexity = dictPC | |||
| } | |||
| sec = Cfg.Section("attachment") | |||
| AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) | |||
| @@ -315,7 +315,7 @@ team_no_units_error = Allow access to at least one repository section. | |||
| email_been_used = The email address is already used. | |||
| openid_been_used = The OpenID address '%s' is already used. | |||
| username_password_incorrect = Username or password is incorrect. | |||
| password_complexity = Password does not pass complexity requirements. | |||
| password_complexity = Password does not pass complexity requirements. | |||
| enterred_invalid_repo_name = The repository name you entered is incorrect. | |||
| enterred_invalid_owner_name = The new owner name is not valid. | |||
| enterred_invalid_password = The password you entered is incorrect. | |||
| @@ -34,7 +34,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) { | |||
| LoginName: "local", | |||
| UserName: username, | |||
| Email: email, | |||
| Password: "xxxxxxxx", | |||
| Password: "abc123ABC!=$", | |||
| SendNotify: false, | |||
| MustChangePassword: true, | |||
| } | |||
| @@ -71,7 +71,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { | |||
| LoginName: "local", | |||
| UserName: username, | |||
| Email: email, | |||
| Password: "xxxxxxxx", | |||
| Password: "abc123ABC!=$", | |||
| SendNotify: false, | |||
| MustChangePassword: false, | |||
| } | |||
| @@ -54,7 +54,7 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { | |||
| } else if form.Password != form.Retype { | |||
| ctx.Flash.Error(ctx.Tr("form.password_not_match")) | |||
| } else if !password.IsComplexEnough(form.Password) { | |||
| ctx.Flash.Error(ctx.Tr("settings.password_complexity")) | |||
| ctx.Flash.Error(ctx.Tr("form.password_complexity")) | |||
| } else { | |||
| var err error | |||
| if ctx.User.Salt, err = models.GetUserSalt(); err != nil { | |||
| @@ -19,76 +19,64 @@ import ( | |||
| func TestChangePassword(t *testing.T) { | |||
| oldPassword := "password" | |||
| setting.MinPasswordLength = 6 | |||
| setting.PasswordComplexity = map[string]string{ | |||
| "lower": "[a-z]+", | |||
| "upper": "[A-Z]+", | |||
| "digit": "[0-9]+", | |||
| "spec": "[-_]+", | |||
| } | |||
| var pcLUN = map[string]string{ | |||
| "lower": "[a-z]+", | |||
| "upper": "[A-Z]+", | |||
| "digit": "[0-9]+", | |||
| } | |||
| var pcLU = map[string]string{ | |||
| "lower": "[a-z]+", | |||
| "upper": "[A-Z]+", | |||
| } | |||
| var pcALL = []string{"lower", "upper", "digit", "spec"} | |||
| var pcLUN = []string{"lower", "upper", "digit"} | |||
| var pcLU = []string{"lower", "upper"} | |||
| for _, req := range []struct { | |||
| OldPassword string | |||
| NewPassword string | |||
| Retype string | |||
| Message string | |||
| PasswordComplexity map[string]string | |||
| PasswordComplexity []string | |||
| }{ | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "Qwerty123456-", | |||
| Retype: "Qwerty123456-", | |||
| Message: "", | |||
| PasswordComplexity: setting.PasswordComplexity, | |||
| PasswordComplexity: pcALL, | |||
| }, | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "12345", | |||
| Retype: "12345", | |||
| Message: "auth.password_too_short", | |||
| PasswordComplexity: setting.PasswordComplexity, | |||
| PasswordComplexity: pcALL, | |||
| }, | |||
| { | |||
| OldPassword: "12334", | |||
| NewPassword: "123456", | |||
| Retype: "123456", | |||
| Message: "settings.password_incorrect", | |||
| PasswordComplexity: setting.PasswordComplexity, | |||
| PasswordComplexity: pcALL, | |||
| }, | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "123456", | |||
| Retype: "12345", | |||
| Message: "form.password_not_match", | |||
| PasswordComplexity: setting.PasswordComplexity, | |||
| PasswordComplexity: pcALL, | |||
| }, | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "Qwerty", | |||
| Retype: "Qwerty", | |||
| Message: "settings.password_complexity", | |||
| PasswordComplexity: setting.PasswordComplexity, | |||
| Message: "form.password_complexity", | |||
| PasswordComplexity: pcALL, | |||
| }, | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "Qwerty", | |||
| Retype: "Qwerty", | |||
| Message: "settings.password_complexity", | |||
| Message: "form.password_complexity", | |||
| PasswordComplexity: pcLUN, | |||
| }, | |||
| { | |||
| OldPassword: oldPassword, | |||
| NewPassword: "QWERTY", | |||
| Retype: "QWERTY", | |||
| Message: "settings.password_complexity", | |||
| Message: "form.password_complexity", | |||
| PasswordComplexity: pcLU, | |||
| }, | |||
| } { | |||