mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Force user to change password (#4489)
* redirect to login page after successfully activating account * force users to change password if account was created by an admin * force users to change password if account was created by an admin * fixed build * fixed build * fix pending issues with translation and wrong routes * make sure path check is safe * remove unneccessary newline * make sure users that don't have to view the form get redirected * move route to use /settings prefix so as to make sure unauthenticated users can't view the page * update as per @lafriks review * add necessary comment * remove unrelated changes * support redirecting to location the user actually want to go to before being forced to change his/her password * run make fmt * added tests * improve assertions * add assertion * fix copyright year Signed-off-by: Lanre Adelowo <yo@lanre.wtf>
This commit is contained in:
		| @@ -198,6 +198,8 @@ var migrations = []Migration{ | |||||||
| 	NewMigration("protect each scratch token", addScratchHash), | 	NewMigration("protect each scratch token", addScratchHash), | ||||||
| 	// v72 -> v73 | 	// v72 -> v73 | ||||||
| 	NewMigration("add review", addReview), | 	NewMigration("add review", addReview), | ||||||
|  | 	// v73 -> v74 | ||||||
|  | 	NewMigration("add must_change_password column for users table", addMustChangePassword), | ||||||
| } | } | ||||||
|  |  | ||||||
| // Migrate database to current version | // Migrate database to current version | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								models/migrations/v73.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								models/migrations/v73.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | // Copyright 2018 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 migrations | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/go-xorm/xorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func addMustChangePassword(x *xorm.Engine) error { | ||||||
|  | 	// User see models/user.go | ||||||
|  | 	type User struct { | ||||||
|  | 		ID                 int64 `xorm:"pk autoincr"` | ||||||
|  | 		MustChangePassword bool  `xorm:"NOT NULL DEFAULT false"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return x.Sync2(new(User)) | ||||||
|  | } | ||||||
| @@ -83,18 +83,23 @@ type User struct { | |||||||
| 	Email            string `xorm:"NOT NULL"` | 	Email            string `xorm:"NOT NULL"` | ||||||
| 	KeepEmailPrivate bool | 	KeepEmailPrivate bool | ||||||
| 	Passwd           string `xorm:"NOT NULL"` | 	Passwd           string `xorm:"NOT NULL"` | ||||||
| 	LoginType        LoginType |  | ||||||
| 	LoginSource      int64 `xorm:"NOT NULL DEFAULT 0"` | 	// MustChangePassword is an attribute that determines if a user | ||||||
| 	LoginName        string | 	// is to change his/her password after registration. | ||||||
| 	Type             UserType | 	MustChangePassword bool `xorm:"NOT NULL DEFAULT false"` | ||||||
| 	OwnedOrgs        []*User       `xorm:"-"` |  | ||||||
| 	Orgs             []*User       `xorm:"-"` | 	LoginType   LoginType | ||||||
| 	Repos            []*Repository `xorm:"-"` | 	LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` | ||||||
| 	Location         string | 	LoginName   string | ||||||
| 	Website          string | 	Type        UserType | ||||||
| 	Rands            string `xorm:"VARCHAR(10)"` | 	OwnedOrgs   []*User       `xorm:"-"` | ||||||
| 	Salt             string `xorm:"VARCHAR(10)"` | 	Orgs        []*User       `xorm:"-"` | ||||||
| 	Language         string `xorm:"VARCHAR(5)"` | 	Repos       []*Repository `xorm:"-"` | ||||||
|  | 	Location    string | ||||||
|  | 	Website     string | ||||||
|  | 	Rands       string `xorm:"VARCHAR(10)"` | ||||||
|  | 	Salt        string `xorm:"VARCHAR(10)"` | ||||||
|  | 	Language    string `xorm:"VARCHAR(5)"` | ||||||
|  |  | ||||||
| 	CreatedUnix   util.TimeStamp `xorm:"INDEX created"` | 	CreatedUnix   util.TimeStamp `xorm:"INDEX created"` | ||||||
| 	UpdatedUnix   util.TimeStamp `xorm:"INDEX updated"` | 	UpdatedUnix   util.TimeStamp `xorm:"INDEX updated"` | ||||||
|   | |||||||
| @@ -84,6 +84,18 @@ func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi | |||||||
| 	return validate(errs, ctx.Data, f, ctx.Locale) | 	return validate(errs, ctx.Data, f, ctx.Locale) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // MustChangePasswordForm form for updating your password after account creation | ||||||
|  | // by an admin | ||||||
|  | type MustChangePasswordForm struct { | ||||||
|  | 	Password string `binding:"Required;MaxSize(255)"` | ||||||
|  | 	Retype   string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Validate valideates the fields | ||||||
|  | func (f *MustChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||||
|  | 	return validate(errs, ctx.Data, f, ctx.Locale) | ||||||
|  | } | ||||||
|  |  | ||||||
| // SignInForm form for signing in with user/password | // SignInForm form for signing in with user/password | ||||||
| type SignInForm struct { | type SignInForm struct { | ||||||
| 	UserName string `binding:"Required;MaxSize(254)"` | 	UserName string `binding:"Required;MaxSize(254)"` | ||||||
|   | |||||||
| @@ -31,10 +31,31 @@ func Toggle(options *ToggleOptions) macaron.Handler { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Check prohibit login users. | 		// Check prohibit login users. | ||||||
| 		if ctx.IsSigned && ctx.User.ProhibitLogin { | 		if ctx.IsSigned { | ||||||
| 			ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") |  | ||||||
| 			ctx.HTML(200, "user/auth/prohibit_login") | 			if ctx.User.ProhibitLogin { | ||||||
| 			return | 				ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") | ||||||
|  | 				ctx.HTML(200, "user/auth/prohibit_login") | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// prevent infinite redirection | ||||||
|  | 			// also make sure that the form cannot be accessed by | ||||||
|  | 			// users who don't need this | ||||||
|  | 			if ctx.Req.URL.Path == setting.AppSubURL+"/user/settings/change_password" { | ||||||
|  | 				if !ctx.User.MustChangePassword { | ||||||
|  | 					ctx.Redirect(setting.AppSubURL + "/") | ||||||
|  | 				} | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if ctx.User.MustChangePassword { | ||||||
|  | 				ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | ||||||
|  | 				ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password" | ||||||
|  | 				ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubURL+ctx.Req.RequestURI), 0, setting.AppSubURL) | ||||||
|  | 				ctx.Redirect(setting.AppSubURL + "/user/settings/change_password") | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Redirect to dashboard if user tries to visit any non-login page. | 		// Redirect to dashboard if user tries to visit any non-login page. | ||||||
|   | |||||||
| @@ -205,6 +205,7 @@ forgot_password = Forgot password? | |||||||
| sign_up_now = Need an account? Register now. | sign_up_now = Need an account? Register now. | ||||||
| sign_up_successful = Account was successfully created. | sign_up_successful = Account was successfully created. | ||||||
| confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. | confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. | ||||||
|  | must_change_password = Update your password | ||||||
| reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the password reset process. | reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the password reset process. | ||||||
| active_your_account = Activate Your Account | active_your_account = Activate Your Account | ||||||
| account_activated = Account has been activated | account_activated = Account has been activated | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								routers/admin/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								routers/admin/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | // Copyright 2018 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 admin | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestMain(m *testing.M) { | ||||||
|  | 	models.MainTest(m, filepath.Join("..", "..")) | ||||||
|  | } | ||||||
| @@ -77,11 +77,12 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	u := &models.User{ | 	u := &models.User{ | ||||||
| 		Name:      form.UserName, | 		Name:               form.UserName, | ||||||
| 		Email:     form.Email, | 		Email:              form.Email, | ||||||
| 		Passwd:    form.Password, | 		Passwd:             form.Password, | ||||||
| 		IsActive:  true, | 		IsActive:           true, | ||||||
| 		LoginType: models.LoginPlain, | 		LoginType:          models.LoginPlain, | ||||||
|  | 		MustChangePassword: true, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(form.LoginType) > 0 { | 	if len(form.LoginType) > 0 { | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								routers/admin/users_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								routers/admin/users_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | // Copyright 2017 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 admin | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/auth" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestNewUserPost_MustChangePassword(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	models.PrepareTestEnv(t) | ||||||
|  | 	ctx := test.MockContext(t, "admin/users/new") | ||||||
|  |  | ||||||
|  | 	u := models.AssertExistsAndLoadBean(t, &models.User{ | ||||||
|  | 		IsAdmin: true, | ||||||
|  | 		ID:      2, | ||||||
|  | 	}).(*models.User) | ||||||
|  |  | ||||||
|  | 	ctx.User = u | ||||||
|  |  | ||||||
|  | 	username := "gitea" | ||||||
|  | 	email := "gitea@gitea.io" | ||||||
|  |  | ||||||
|  | 	form := auth.AdminCreateUserForm{ | ||||||
|  | 		LoginType:  "local", | ||||||
|  | 		LoginName:  "local", | ||||||
|  | 		UserName:   username, | ||||||
|  | 		Email:      email, | ||||||
|  | 		Password:   "xxxxxxxx", | ||||||
|  | 		SendNotify: false, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	NewUserPost(ctx, form) | ||||||
|  |  | ||||||
|  | 	assert.NotEmpty(t, ctx.Flash.SuccessMsg) | ||||||
|  |  | ||||||
|  | 	u, err := models.GetUserByName(username) | ||||||
|  |  | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, username, u.Name) | ||||||
|  | 	assert.Equal(t, email, u.Email) | ||||||
|  | 	assert.True(t, u.MustChangePassword) | ||||||
|  | } | ||||||
| @@ -230,6 +230,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 	m.Group("/user/settings", func() { | 	m.Group("/user/settings", func() { | ||||||
| 		m.Get("", userSetting.Profile) | 		m.Get("", userSetting.Profile) | ||||||
| 		m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | 		m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | ||||||
|  | 		m.Get("/change_password", user.MustChangePassword) | ||||||
|  | 		m.Post("/change_password", bindIgnErr(auth.MustChangePasswordForm{}), user.MustChangePasswordPost) | ||||||
| 		m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) | 		m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) | ||||||
| 		m.Post("/avatar/delete", userSetting.DeleteAvatar) | 		m.Post("/avatar/delete", userSetting.DeleteAvatar) | ||||||
| 		m.Group("/account", func() { | 		m.Group("/account", func() { | ||||||
|   | |||||||
| @@ -28,6 +28,8 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|  | 	// tplMustChangePassword template for updating a user's password | ||||||
|  | 	tplMustChangePassword = "user/auth/change_passwd" | ||||||
| 	// tplSignIn template for sign in page | 	// tplSignIn template for sign in page | ||||||
| 	tplSignIn base.TplName = "user/auth/signin" | 	tplSignIn base.TplName = "user/auth/signin" | ||||||
| 	// tplSignUp template path for sign up page | 	// tplSignUp template path for sign up page | ||||||
| @@ -1178,7 +1180,8 @@ func ResetPasswdPost(ctx *context.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		u.HashPassword(passwd) | 		u.HashPassword(passwd) | ||||||
| 		if err := models.UpdateUserCols(u, "passwd", "rands", "salt"); err != nil { | 		u.MustChangePassword = false | ||||||
|  | 		if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil { | ||||||
| 			ctx.ServerError("UpdateUser", err) | 			ctx.ServerError("UpdateUser", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -1191,3 +1194,71 @@ func ResetPasswdPost(ctx *context.Context) { | |||||||
| 	ctx.Data["IsResetFailed"] = true | 	ctx.Data["IsResetFailed"] = true | ||||||
| 	ctx.HTML(200, tplResetPassword) | 	ctx.HTML(200, tplResetPassword) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // MustChangePassword renders the page to change a user's password | ||||||
|  | func MustChangePassword(ctx *context.Context) { | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | ||||||
|  | 	ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" | ||||||
|  |  | ||||||
|  | 	ctx.HTML(200, tplMustChangePassword) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MustChangePasswordPost response for updating a user's password after his/her | ||||||
|  | // account was created by an admin | ||||||
|  | func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form auth.MustChangePasswordForm) { | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | ||||||
|  |  | ||||||
|  | 	ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" | ||||||
|  |  | ||||||
|  | 	if ctx.HasError() { | ||||||
|  | 		ctx.HTML(200, tplMustChangePassword) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u := ctx.User | ||||||
|  |  | ||||||
|  | 	// Make sure only requests for users who are eligible to change their password via | ||||||
|  | 	// this method passes through | ||||||
|  | 	if !u.MustChangePassword { | ||||||
|  | 		ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if form.Password != form.Retype { | ||||||
|  | 		ctx.Data["Err_Password"] = true | ||||||
|  | 		ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(form.Password) < setting.MinPasswordLength { | ||||||
|  | 		ctx.Data["Err_Password"] = true | ||||||
|  | 		ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	if u.Salt, err = models.GetUserSalt(); err != nil { | ||||||
|  | 		ctx.ServerError("UpdateUser", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u.HashPassword(form.Password) | ||||||
|  | 	u.MustChangePassword = false | ||||||
|  |  | ||||||
|  | 	if err := models.UpdateUserCols(u, "must_change_password", "passwd", "salt"); err != nil { | ||||||
|  | 		ctx.ServerError("UpdateUser", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Flash.Success(ctx.Tr("settings.change_password_success")) | ||||||
|  |  | ||||||
|  | 	log.Trace("User updated password: %s", u.Name) | ||||||
|  |  | ||||||
|  | 	if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 && !util.IsExternalURL(redirectTo) { | ||||||
|  | 		ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) | ||||||
|  | 		ctx.RedirectToFirst(redirectTo) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Redirect(setting.AppSubURL + "/") | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								templates/user/auth/change_passwd.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/user/auth/change_passwd.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | {{template "base/head" .}} | ||||||
|  | <div class="user signin{{if .LinkAccountMode}} icon{{end}}"> | ||||||
|  | 	<div class="ui container"> | ||||||
|  | 		{{template "user/auth/change_passwd_inner" .}} | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | {{template "base/footer" .}} | ||||||
							
								
								
									
										26
									
								
								templates/user/auth/change_passwd_inner.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								templates/user/auth/change_passwd_inner.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | 		{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} | ||||||
|  | 		{{template "base/alert" .}} | ||||||
|  | 		{{end}} | ||||||
|  | 		<h4 class="ui top attached header center"> | ||||||
|  | 			{{.i18n.Tr "settings.change_password"}} | ||||||
|  | 		</h4> | ||||||
|  | 		<div class="ui attached segment"> | ||||||
|  | 			<form class="ui form" action="{{.ChangePasscodeLink}}" method="post"> | ||||||
|  | 			{{.CsrfTokenHtml}} | ||||||
|  | 			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> | ||||||
|  | 				<label for="password">{{.i18n.Tr "password"}}</label> | ||||||
|  | 				<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> | ||||||
|  | 				<label for="retype">{{.i18n.Tr "re_type"}}</label> | ||||||
|  | 				<input id="retype" name="retype" type="password" autocomplete="off" required> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="inline field"> | ||||||
|  | 				<label></label> | ||||||
|  | 				<button class="ui green button">{{.i18n.Tr "settings.change_password" }}</button> | ||||||
|  | 			</div> | ||||||
|  | 			</form> | ||||||
|  | 		</div> | ||||||
		Reference in New Issue
	
	Block a user