Add password requirement info on error (#9074)
* 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>
This commit is contained in:
		
							parent
							
								
									eb0359cad4
								
							
						
					
					
						commit
						c57edb6c7b
					
				
					 9 changed files with 72 additions and 24 deletions
				
			
		|  | @ -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{ | 	charComplexities = map[string]complexity{ | ||||||
| 		"lower": `abcdefghijklmnopqrstuvwxyz`, | 		"lower": { | ||||||
| 		"upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, | 			`abcdefghijklmnopqrstuvwxyz`, | ||||||
| 		"digit": `0123456789`, | 			"form.password_lowercase_one", | ||||||
| 		"spec":  ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", | 		}, | ||||||
|  | 		"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 { | 			if complex, ok := charComplexities[val]; ok { | ||||||
| 				validChars += chars | 				validChars += complex.ValidChars | ||||||
| 				requiredChars = append(requiredChars, chars) | 				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 { | 			for _, complex := range charComplexities { | ||||||
| 				validChars += chars | 				validChars += complex.ValidChars | ||||||
| 				requiredChars = append(requiredChars, chars) | 				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 { | 		for _, req := range requiredList { | ||||||
| 			if !strings.ContainsAny(req, pwd) { | 			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; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue