* Add password requirement info on error * Move BuildComplexityError to the password pkg * Unexport complexity type * Fix extra line * Update modules/password/password.go Co-Authored-By: Lauris BH <lauris@nix.lv>tags/v1.21.12.1
| @@ -5,24 +5,44 @@ | |||||
| package password | package password | ||||
| import ( | import ( | ||||
| "bytes" | |||||
| "crypto/rand" | "crypto/rand" | ||||
| "math/big" | "math/big" | ||||
| "strings" | "strings" | ||||
| "sync" | "sync" | ||||
| "code.gitea.io/gitea/modules/context" | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| ) | ) | ||||
| // complexity contains information about a particular kind of password complexity | |||||
| type complexity struct { | |||||
| ValidChars string | |||||
| TrNameOne string | |||||
| } | |||||
| var ( | var ( | ||||
| matchComplexityOnce sync.Once | matchComplexityOnce sync.Once | ||||
| validChars string | validChars string | ||||
| requiredChars []string | |||||
| requiredList []complexity | |||||
| charComplexities = map[string]string{ | |||||
| "lower": `abcdefghijklmnopqrstuvwxyz`, | |||||
| "upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, | |||||
| "digit": `0123456789`, | |||||
| "spec": ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", | |||||
| charComplexities = map[string]complexity{ | |||||
| "lower": { | |||||
| `abcdefghijklmnopqrstuvwxyz`, | |||||
| "form.password_lowercase_one", | |||||
| }, | |||||
| "upper": { | |||||
| `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, | |||||
| "form.password_uppercase_one", | |||||
| }, | |||||
| "digit": { | |||||
| `0123456789`, | |||||
| "form.password_digit_one", | |||||
| }, | |||||
| "spec": { | |||||
| ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", | |||||
| "form.password_special_one", | |||||
| }, | |||||
| } | } | ||||
| ) | ) | ||||
| @@ -36,22 +56,22 @@ func NewComplexity() { | |||||
| func setupComplexity(values []string) { | func setupComplexity(values []string) { | ||||
| if len(values) != 1 || values[0] != "off" { | if len(values) != 1 || values[0] != "off" { | ||||
| for _, val := range values { | for _, val := range values { | ||||
| if chars, ok := charComplexities[val]; ok { | |||||
| validChars += chars | |||||
| requiredChars = append(requiredChars, chars) | |||||
| if complex, ok := charComplexities[val]; ok { | |||||
| validChars += complex.ValidChars | |||||
| requiredList = append(requiredList, complex) | |||||
| } | } | ||||
| } | } | ||||
| if len(requiredChars) == 0 { | |||||
| if len(requiredList) == 0 { | |||||
| // No valid character classes found; use all classes as default | // No valid character classes found; use all classes as default | ||||
| for _, chars := range charComplexities { | |||||
| validChars += chars | |||||
| requiredChars = append(requiredChars, chars) | |||||
| for _, complex := range charComplexities { | |||||
| validChars += complex.ValidChars | |||||
| requiredList = append(requiredList, complex) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if validChars == "" { | if validChars == "" { | ||||
| // No complexities to check; provide a sensible default for password generation | // No complexities to check; provide a sensible default for password generation | ||||
| validChars = charComplexities["lower"] + charComplexities["upper"] + charComplexities["digit"] | |||||
| validChars = charComplexities["lower"].ValidChars + charComplexities["upper"].ValidChars + charComplexities["digit"].ValidChars | |||||
| } | } | ||||
| } | } | ||||
| @@ -59,8 +79,8 @@ func setupComplexity(values []string) { | |||||
| func IsComplexEnough(pwd string) bool { | func IsComplexEnough(pwd string) bool { | ||||
| NewComplexity() | NewComplexity() | ||||
| if len(validChars) > 0 { | if len(validChars) > 0 { | ||||
| for _, req := range requiredChars { | |||||
| if !strings.ContainsAny(req, pwd) { | |||||
| for _, req := range requiredList { | |||||
| if !strings.ContainsAny(req.ValidChars, pwd) { | |||||
| return false | return false | ||||
| } | } | ||||
| } | } | ||||
| @@ -86,3 +106,17 @@ func Generate(n int) (string, error) { | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // BuildComplexityError builds the error message when password complexity checks fail | |||||
| func BuildComplexityError(ctx *context.Context) string { | |||||
| var buffer bytes.Buffer | |||||
| buffer.WriteString(ctx.Tr("form.password_complexity")) | |||||
| buffer.WriteString("<ul>") | |||||
| for _, c := range requiredList { | |||||
| buffer.WriteString("<li>") | |||||
| buffer.WriteString(ctx.Tr(c.TrNameOne)) | |||||
| buffer.WriteString("</li>") | |||||
| } | |||||
| buffer.WriteString("</ul>") | |||||
| return buffer.String() | |||||
| } | |||||
| @@ -18,6 +18,7 @@ func TestComplexity_IsComplexEnough(t *testing.T) { | |||||
| truevalues []string | truevalues []string | ||||
| falsevalues []string | falsevalues []string | ||||
| }{ | }{ | ||||
| {[]string{"off"}, []string{"1", "-", "a", "A", "ñ", "日本語"}, []string{}}, | |||||
| {[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, | {[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, | ||||
| {[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, | {[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, | ||||
| {[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, | {[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, | ||||
| @@ -25,6 +26,7 @@ func TestComplexity_IsComplexEnough(t *testing.T) { | |||||
| {[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, | {[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, | ||||
| {[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, | {[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, | ||||
| {[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, | {[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, | ||||
| {[]string{""}, []string{"abC=1", "abc!9D"}, []string{"ABC", "123", "=!$", ""}}, | |||||
| } | } | ||||
| for _, test := range testlist { | for _, test := range testlist { | ||||
| @@ -70,6 +72,6 @@ func TestComplexity_Generate(t *testing.T) { | |||||
| func testComplextity(values []string) { | func testComplextity(values []string) { | ||||
| // Cleanup previous values | // Cleanup previous values | ||||
| validChars = "" | validChars = "" | ||||
| requiredChars = make([]string, 0, len(values)) | |||||
| requiredList = make([]complexity, 0, len(values)) | |||||
| setupComplexity(values) | setupComplexity(values) | ||||
| } | } | ||||
| @@ -328,7 +328,11 @@ team_no_units_error = Allow access to at least one repository section. | |||||
| email_been_used = The email address is already used. | email_been_used = The email address is already used. | ||||
| openid_been_used = The OpenID address '%s' is already used. | openid_been_used = The OpenID address '%s' is already used. | ||||
| username_password_incorrect = Username or password is incorrect. | username_password_incorrect = Username or password is incorrect. | ||||
| password_complexity = Password does not pass complexity requirements. | |||||
| password_complexity = Password does not pass complexity requirements: | |||||
| password_lowercase_one = At least one lowercase character | |||||
| password_uppercase_one = At least one uppercase character | |||||
| password_digit_one = At least one digit | |||||
| password_special_one = At least one special character (punctuation, brackets, quotes, etc.) | |||||
| enterred_invalid_repo_name = The repository name you entered is incorrect. | enterred_invalid_repo_name = The repository name you entered is incorrect. | ||||
| enterred_invalid_owner_name = The new owner name is not valid. | enterred_invalid_owner_name = The new owner name is not valid. | ||||
| enterred_invalid_password = The password you entered is incorrect. | enterred_invalid_password = The password you entered is incorrect. | ||||
| @@ -113,6 +113,7 @@ a{cursor:pointer} | |||||
| .ui .text.nopadding{padding:0} | .ui .text.nopadding{padding:0} | ||||
| .ui .text.nomargin{margin:0} | .ui .text.nomargin{margin:0} | ||||
| .ui .message{text-align:center} | .ui .message{text-align:center} | ||||
| .ui .message>ul{margin-left:auto;margin-right:auto;display:table;text-align:left} | |||||
| .ui.bottom.attached.message{font-weight:700;text-align:left;color:#000} | .ui.bottom.attached.message{font-weight:700;text-align:left;color:#000} | ||||
| .ui.bottom.attached.message .pull-right{color:#000} | .ui.bottom.attached.message .pull-right{color:#000} | ||||
| .ui.bottom.attached.message .pull-right>span,.ui.bottom.attached.message>span{color:#21ba45} | .ui.bottom.attached.message .pull-right>span,.ui.bottom.attached.message>span{color:#21ba45} | ||||
| @@ -96,7 +96,7 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { | |||||
| } | } | ||||
| if u.LoginType == models.LoginPlain { | if u.LoginType == models.LoginPlain { | ||||
| if !password.IsComplexEnough(form.Password) { | if !password.IsComplexEnough(form.Password) { | ||||
| ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserNew, &form) | |||||
| ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form) | |||||
| return | return | ||||
| } | } | ||||
| u.MustChangePassword = form.MustChangePassword | u.MustChangePassword = form.MustChangePassword | ||||
| @@ -208,7 +208,7 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { | |||||
| return | return | ||||
| } | } | ||||
| if !password.IsComplexEnough(form.Password) { | if !password.IsComplexEnough(form.Password) { | ||||
| ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserEdit, &form) | |||||
| ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form) | |||||
| return | return | ||||
| } | } | ||||
| u.HashPassword(form.Password) | u.HashPassword(form.Password) | ||||
| @@ -1072,7 +1072,7 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo | |||||
| } | } | ||||
| if !password.IsComplexEnough(form.Password) { | if !password.IsComplexEnough(form.Password) { | ||||
| ctx.Data["Err_Password"] = true | ctx.Data["Err_Password"] = true | ||||
| ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplSignUp, &form) | |||||
| ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form) | |||||
| return | return | ||||
| } | } | ||||
| @@ -1343,7 +1343,7 @@ func ResetPasswdPost(ctx *context.Context) { | |||||
| } else if !password.IsComplexEnough(passwd) { | } else if !password.IsComplexEnough(passwd) { | ||||
| ctx.Data["IsResetForm"] = true | ctx.Data["IsResetForm"] = true | ||||
| ctx.Data["Err_Password"] = true | ctx.Data["Err_Password"] = true | ||||
| ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplResetPassword, nil) | |||||
| ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil) | |||||
| return | return | ||||
| } | } | ||||
| @@ -53,7 +53,7 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { | |||||
| } else if form.Password != form.Retype { | } else if form.Password != form.Retype { | ||||
| ctx.Flash.Error(ctx.Tr("form.password_not_match")) | ctx.Flash.Error(ctx.Tr("form.password_not_match")) | ||||
| } else if !password.IsComplexEnough(form.Password) { | } else if !password.IsComplexEnough(form.Password) { | ||||
| ctx.Flash.Error(ctx.Tr("form.password_complexity")) | |||||
| ctx.Flash.Error(password.BuildComplexityError(ctx)) | |||||
| } else { | } else { | ||||
| var err error | var err error | ||||
| if ctx.User.Salt, err = models.GetUserSalt(); err != nil { | if ctx.User.Salt, err = models.GetUserSalt(); err != nil { | ||||
| @@ -91,7 +91,7 @@ func TestChangePassword(t *testing.T) { | |||||
| Retype: req.Retype, | Retype: req.Retype, | ||||
| }) | }) | ||||
| assert.EqualValues(t, req.Message, ctx.Flash.ErrorMsg) | |||||
| assert.Contains(t, ctx.Flash.ErrorMsg, req.Message) | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | ||||
| } | } | ||||
| } | } | ||||
| @@ -471,6 +471,13 @@ code, | |||||
| text-align: center; | text-align: center; | ||||
| } | } | ||||
| .message > ul { | |||||
| margin-left: auto; | |||||
| margin-right: auto; | |||||
| display: table; | |||||
| text-align: left; | |||||
| } | |||||
| &.bottom.attached.message { | &.bottom.attached.message { | ||||
| font-weight: bold; | font-weight: bold; | ||||
| text-align: left; | text-align: left; | ||||