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>release/v1.15
parent
10a2a904d7
commit
126ba796dc
|
@ -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
|
||||||
|
|
|
@ -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,6 +83,11 @@ 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"`
|
||||||
|
|
||||||
|
// MustChangePassword is an attribute that determines if a user
|
||||||
|
// is to change his/her password after registration.
|
||||||
|
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
|
||||||
LoginType LoginType
|
LoginType LoginType
|
||||||
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
|
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
LoginName string
|
LoginName string
|
||||||
|
|
|
@ -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,12 +31,33 @@ func Toggle(options *ToggleOptions) macaron.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check prohibit login users.
|
// Check prohibit login users.
|
||||||
if ctx.IsSigned && ctx.User.ProhibitLogin {
|
if ctx.IsSigned {
|
||||||
|
|
||||||
|
if ctx.User.ProhibitLogin {
|
||||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||||
ctx.HTML(200, "user/auth/prohibit_login")
|
ctx.HTML(200, "user/auth/prohibit_login")
|
||||||
return
|
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.
|
||||||
if options.SignOutRequired && ctx.IsSigned && ctx.Req.RequestURI != "/" {
|
if options.SignOutRequired && ctx.IsSigned && ctx.Req.RequestURI != "/" {
|
||||||
ctx.Redirect(setting.AppSubURL + "/")
|
ctx.Redirect(setting.AppSubURL + "/")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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("..", ".."))
|
||||||
|
}
|
|
@ -82,6 +82,7 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) {
|
||||||
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 {
|
||||||
|
|
|
@ -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 + "/")
|
||||||
|
}
|
||||||
|
|
|
@ -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" .}}
|
|
@ -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>
|
Loading…
Reference in New Issue