[MM-13840] Change eMail as a post-verification action (#10253)

* Change eMail as a post-verification action

* Fix broken test

* comment for special behavior, tests

* govet

* Check for already existent eMails when require email verification is turned on before accepting update
This commit is contained in:
Daniel Schalla
2019-02-20 15:50:52 +01:00
committed by GitHub
parent 1218e774ba
commit f046163a12
16 changed files with 190 additions and 48 deletions

View File

@@ -294,7 +294,7 @@ func (me *TestHelper) CreateUserWithClient(client *model.Client4) *model.User {
}
ruser.Password = "Password1"
store.Must(me.App.Srv.Store.User().VerifyEmail(ruser.Id))
store.Must(me.App.Srv.Store.User().VerifyEmail(ruser.Id, ruser.Email))
utils.EnableDebugLogForTest()
return ruser
}

View File

@@ -1355,7 +1355,7 @@ func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if err = c.App.SendEmailVerification(user); err != nil {
if err = c.App.SendEmailVerification(user, user.Email); err != nil {
// Don't want to leak whether the email is valid or not
mlog.Error(err.Error())
ReturnStatusOK(w)

View File

@@ -1220,7 +1220,7 @@ func TestUpdateUserAuth(t *testing.T) {
user := th.CreateUser()
th.LinkUserToTeam(user, team)
store.Must(th.App.Srv.Store.User().VerifyEmail(user.Id))
store.Must(th.App.Srv.Store.User().VerifyEmail(user.Id, user.Email))
userAuth := &model.UserAuth{}
userAuth.AuthData = user.AuthData
@@ -1260,7 +1260,7 @@ func TestUpdateUserAuth(t *testing.T) {
// Regular user can not use endpoint
user2 := th.CreateUser()
th.LinkUserToTeam(user2, team)
store.Must(th.App.Srv.Store.User().VerifyEmail(user2.Id))
store.Must(th.App.Srv.Store.User().VerifyEmail(user2.Id, user2.Email))
th.SystemAdminClient.Login(user2.Email, "passwd1")
@@ -2220,11 +2220,12 @@ func TestVerifyUserEmail(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Darth Vader", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID}
email := th.GenerateTestEmail()
user := model.User{Email: email, Nickname: "Darth Vader", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID}
ruser, _ := th.Client.CreateUser(&user)
token, err := th.App.CreateVerifyEmailToken(ruser.Id)
token, err := th.App.CreateVerifyEmailToken(ruser.Id, email)
if err != nil {
t.Fatal("Unable to create email verify token")
}

View File

@@ -304,12 +304,12 @@ func TestWebSocketStatuses(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user)).(*model.User)
th.LinkUserToTeam(ruser, rteam)
store.Must(th.App.Srv.Store.User().VerifyEmail(ruser.Id))
store.Must(th.App.Srv.Store.User().VerifyEmail(ruser.Id, ruser.Email))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser2 := Client.Must(Client.CreateUser(&user2)).(*model.User)
th.LinkUserToTeam(ruser2, rteam)
store.Must(th.App.Srv.Store.User().VerifyEmail(ruser2.Id))
store.Must(th.App.Srv.Store.User().VerifyEmail(ruser2.Id, ruser2.Email))
Client.Login(user.Email, user.Password)

View File

@@ -48,7 +48,7 @@ func (a *App) CreateBasicUser(client *model.Client4) *model.AppError {
if resp.Error != nil {
return resp.Error
}
store.Must(a.Srv.Store.User().VerifyEmail(ruser.Id))
store.Must(a.Srv.Store.User().VerifyEmail(ruser.Id, ruser.Email))
store.Must(a.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id}, *a.Config().TeamSettings.MaxUsersPerTeam))
}
return nil
@@ -83,7 +83,7 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) {
}
// We need to cheat to verify the user's email
store.Must(cfg.app.Srv.Store.User().VerifyEmail(ruser.Id))
store.Must(cfg.app.Srv.Store.User().VerifyEmail(ruser.Id, ruser.Email))
return ruser, true
}

View File

@@ -180,7 +180,7 @@ func (a *App) SendWelcomeEmail(userId string, email string, verified bool, local
}
if !verified {
token, err := a.CreateVerifyEmailToken(userId)
token, err := a.CreateVerifyEmailToken(userId, email)
if err != nil {
return err
}

View File

@@ -477,7 +477,7 @@ func (a *App) ImportUser(data *UserImportData, dryRun bool) *model.AppError {
}
if emailVerified {
if hasUserEmailVerifiedChanged {
if err := a.VerifyUserEmail(user.Id); err != nil {
if err := a.VerifyUserEmail(user.Id, user.Email); err != nil {
return err
}
}

View File

@@ -746,7 +746,7 @@ func (a *App) OldImportUser(team *model.Team, user *model.User) *model.User {
}
ruser := result.Data.(*model.User)
if cresult := <-a.Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
if cresult := <-a.Srv.Store.User().VerifyEmail(ruser.Id, ruser.Email); cresult.Err != nil {
mlog.Error(fmt.Sprintf("Failed to set email verified err=%v", cresult.Err))
}

View File

@@ -6,6 +6,7 @@ package app
import (
"bytes"
b64 "encoding/base64"
"encoding/json"
"fmt"
"hash/fnv"
"image"
@@ -243,7 +244,7 @@ func (a *App) createUser(user *model.User) (*model.User, *model.AppError) {
ruser := result.Data.(*model.User)
if user.EmailVerified {
if err := a.VerifyUserEmail(ruser.Id); err != nil {
if err := a.VerifyUserEmail(ruser.Id, user.Email); err != nil {
mlog.Error(fmt.Sprintf("Failed to set email verified err=%v", err))
}
}
@@ -996,34 +997,49 @@ func (a *App) sendUpdatedUserEvent(user model.User) {
}
func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
result := <-a.Srv.Store.User().Get(user.Id)
if result.Err != nil {
return nil, result.Err
}
prev := result.Data.(*model.User)
if !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
result := <-a.Srv.Store.User().Get(user.Id)
if result.Err != nil {
return nil, result.Err
}
prev := result.Data.(*model.User)
if !prev.IsLDAPUser() && !prev.IsSAMLUser() && user.Email != prev.Email {
return nil, model.NewAppError("UpdateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
}
result := <-a.Srv.Store.User().Update(user, false)
// Don't set new eMail on user account if email verification is required, this will be done as a post-verification action
// to avoid users being able to set non-controlled eMails as their account email
newEmail := ""
if *a.Config().EmailSettings.RequireEmailVerification && prev.Email != user.Email {
newEmail = user.Email
_, err := a.GetUserByEmail(newEmail)
if err == nil {
return nil, model.NewAppError("UpdateUser", "store.sql_user.update.email_taken.app_error", nil, "user_id="+user.Id, http.StatusBadRequest)
}
user.Email = prev.Email
}
result = <-a.Srv.Store.User().Update(user, false)
if result.Err != nil {
return nil, result.Err
}
rusers := result.Data.([2]*model.User)
if sendNotifications {
if rusers[0].Email != rusers[1].Email {
a.Srv.Go(func() {
if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
mlog.Error(err.Error())
}
})
if rusers[0].Email != rusers[1].Email || newEmail != "" {
if *a.Config().EmailSettings.RequireEmailVerification {
a.Srv.Go(func() {
if err := a.SendEmailVerification(rusers[0]); err != nil {
if err := a.SendEmailVerification(rusers[0], newEmail); err != nil {
mlog.Error(err.Error())
}
})
} else {
a.Srv.Go(func() {
if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
mlog.Error(err.Error())
}
})
@@ -1366,16 +1382,16 @@ func (a *App) PermanentDeleteAllUsers() *model.AppError {
return nil
}
func (a *App) SendEmailVerification(user *model.User) *model.AppError {
token, err := a.CreateVerifyEmailToken(user.Id)
func (a *App) SendEmailVerification(user *model.User, newEmail string) *model.AppError {
token, err := a.CreateVerifyEmailToken(user.Id, newEmail)
if err != nil {
return err
}
if _, err := a.GetStatus(user.Id); err != nil {
return a.SendVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
return a.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token)
}
return a.SendEmailChangeVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
return a.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token)
}
func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError {
@@ -1384,11 +1400,36 @@ func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppErr
return err
}
if model.GetMillis()-token.CreateAt >= PASSWORD_RECOVER_EXPIRY_TIME {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.link_expired.app_error", nil, "", http.StatusBadRequest)
}
if err := a.VerifyUserEmail(token.Extra); err != nil {
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
if err := a.VerifyUserEmail(tokenData.UserId, tokenData.Email); err != nil {
return err
}
if user.Email != tokenData.Email {
a.Srv.Go(func() {
if err := a.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error(err.Error())
}
})
}
if err := a.DeleteToken(token); err != nil {
mlog.Error(err.Error())
}
@@ -1396,8 +1437,21 @@ func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppErr
return nil
}
func (a *App) CreateVerifyEmailToken(userId string) (*model.Token, *model.AppError) {
token := model.NewToken(TOKEN_TYPE_VERIFY_EMAIL, userId)
func (a *App) CreateVerifyEmailToken(userId string, newEmail string) (*model.Token, *model.AppError) {
tokenExtra := struct {
UserId string
Email string
}{
userId,
newEmail,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
}
token := model.NewToken(TOKEN_TYPE_VERIFY_EMAIL, string(jsonData))
if result := <-a.Srv.Store.Token().Save(token); result.Err != nil {
return nil, result.Err
@@ -1429,8 +1483,22 @@ func (a *App) GetTotalUsersStats() (*model.UsersStats, *model.AppError) {
return stats, nil
}
func (a *App) VerifyUserEmail(userId string) *model.AppError {
return (<-a.Srv.Store.User().VerifyEmail(userId)).Err
func (a *App) VerifyUserEmail(userId, email string) *model.AppError {
err := (<-a.Srv.Store.User().VerifyEmail(userId, email)).Err
if err != nil {
return err
}
user, err := a.GetUser(userId)
if err != nil {
return err
}
a.sendUpdatedUserEvent(*user)
return nil
}
func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {

View File

@@ -286,6 +286,66 @@ func TestUpdateOAuthUserAttrs(t *testing.T) {
})
}
func TestUpdateUserEmail(t *testing.T) {
th := Setup(t)
defer th.TearDown()
user := th.CreateUser()
t.Run("RequireVerification", func(t *testing.T){
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = true
})
currentEmail := user.Email
newEmail := th.MakeEmail()
user.Email = newEmail
user2, err := th.App.UpdateUser(user, false)
assert.Nil(t, err)
assert.Equal(t, currentEmail, user2.Email)
assert.True(t, user2.EmailVerified)
token, err := th.App.CreateVerifyEmailToken(user2.Id, newEmail)
assert.Nil(t, err)
err = th.App.VerifyEmailFromToken(token.Token)
assert.Nil(t, err)
user2, err = th.App.GetUser(user2.Id)
assert.Nil(t, err)
assert.Equal(t, newEmail, user2.Email)
assert.True(t, user2.EmailVerified)
})
t.Run("RequireVerificationAlreadyUsedEmail", func(t *testing.T){
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = true
})
user2 := th.CreateUser()
newEmail := user2.Email
user.Email = newEmail
user3, err := th.App.UpdateUser(user, false)
assert.NotNil(t, err)
assert.Nil(t, user3)
})
t.Run("NoVerification", func(t *testing.T){
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = false
})
newEmail := th.MakeEmail()
user.Email = newEmail
user2, err := th.App.UpdateUser(user, false)
assert.Nil(t, err)
assert.Equal(t, newEmail, user2.Email)
})
}
func getUserFromDB(a *App, id string, t *testing.T) *model.User {
user, err := a.GetUser(id)
if err != nil {

View File

@@ -693,7 +693,7 @@ func verifyUserCmdF(command *cobra.Command, args []string) error {
CommandPrintErrorln("Unable to find user '" + args[i] + "'")
continue
}
if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil {
if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id, user.Email); cresult.Err != nil {
CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error())
}
}

View File

@@ -2362,6 +2362,18 @@
"id": "api.user.send_email_change_verify_email_and_forget.error",
"translation": "Failed to send email change verification email successfully"
},
{
"id": "api.user.verify_email.link_expired.app_error",
"translation": "The email verification link has expired."
},
{
"id": "api.user.verify_email.token_parse.error",
"translation": "Failed to parse token data from email verification"
},
{
"id": "api.user.create_email_token.error",
"translation": "Failed to create token data for email verification"
},
{
"id": "api.user.send_mfa_change_email.error",
"translation": "Unable to send email notification for MFA change."

View File

@@ -102,7 +102,7 @@ func manualTest(c *web.Context, w http.ResponseWriter, r *http.Request) {
return
}
<-c.App.Srv.Store.User().VerifyEmail(user.Id)
<-c.App.Srv.Store.User().VerifyEmail(user.Id, user.Email)
<-c.App.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, *c.App.Config().TeamSettings.MaxUsersPerTeam)
userID = user.Id

View File

@@ -1025,9 +1025,10 @@ func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allo
})
}
func (us SqlUserStore) VerifyEmail(userId string) store.StoreChannel {
func (us SqlUserStore) VerifyEmail(userId, email string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
if _, err := us.GetMaster().Exec("UPDATE Users SET EmailVerified = true WHERE Id = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
curTime := model.GetMillis()
if _, err := us.GetMaster().Exec("UPDATE Users SET Email = :email, EmailVerified = true, UpdateAt = :Time WHERE Id = :UserId", map[string]interface{}{"email": email, "Time": curTime, "UserId": userId}); err != nil {
result.Err = model.NewAppError("SqlUserStore.VerifyEmail", "store.sql_user.verify_email.app_error", nil, "userId="+userId+", "+err.Error(), http.StatusInternalServerError)
}

View File

@@ -259,7 +259,7 @@ type UserStore interface {
GetAllUsingAuthService(authService string) StoreChannel
GetByUsername(username string) StoreChannel
GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail bool) StoreChannel
VerifyEmail(userId string) StoreChannel
VerifyEmail(userId, email string) StoreChannel
GetEtagForAllProfiles() StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel

View File

@@ -833,13 +833,13 @@ func (_m *UserStore) UpdateUpdateAt(userId string) store.StoreChannel {
return r0
}
// VerifyEmail provides a mock function with given fields: userId
func (_m *UserStore) VerifyEmail(userId string) store.StoreChannel {
ret := _m.Called(userId)
// VerifyEmail provides a mock function with given fields: userId, email
func (_m *UserStore) VerifyEmail(userId string, email string) store.StoreChannel {
ret := _m.Called(userId, email)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
r0 = rf(userId)
if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
r0 = rf(userId, email)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)