[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
Этот коммит содержится в:
Daniel Schalla
2019-02-20 15:50:52 +01:00
коммит произвёл GitHub
родитель 1218e774ba
Коммит f046163a12
16 изменённых файлов: 190 добавлений и 48 удалений

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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")
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}
}

Просмотреть файл

@@ -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))
}

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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())
}
}

Просмотреть файл

@@ -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."

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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)