Refactor: Email verification (#84393)

* Update template names

* Add verifier that we can use to start verify process

* Use userVerifier when verifying email on update

* Add tests
---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Karl Persson
2024-03-14 13:25:28 +01:00
committed by GitHub
co-authored by Ieva
parent 38a8bf10f3
commit 8d9521fb6d
13 changed files with 267 additions and 64 deletions
+3
View File
@@ -217,6 +217,7 @@ type HTTPServer struct {
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
namespacer request.NamespaceMapper
anonService anonymous.Service
userVerifier user.Verifier
}
type ServerOptions struct {
@@ -259,6 +260,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer,
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service,
userVerifier user.Verifier,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -361,6 +363,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
clientConfigProvider: clientConfigProvider,
namespacer: request.GetNamespaceMapper(cfg),
anonService: anonService,
userVerifier: userVerifier,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+11 -58
View File
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
@@ -245,25 +244,22 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC
usr, err := hs.userService.GetByID(ctx, &query)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil)
return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), err)
}
return response.Error(http.StatusInternalServerError, "Failed to get user", err)
}
if len(cmd.Email) != 0 && usr.Email != cmd.Email {
// Email is being updated
newEmail, err := ValidateAndNormalizeEmail(cmd.Email)
normalized, err := ValidateAndNormalizeEmail(cmd.Email)
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid email address", err)
}
return hs.verifyEmailUpdate(ctx, newEmail, user.EmailUpdateAction, usr)
return hs.verifyEmailUpdate(ctx, normalized, user.EmailUpdateAction, usr)
}
if len(cmd.Login) != 0 && usr.Login != cmd.Login {
// Username is being updated. If it's an email, go through the email verification flow
newEmailLogin, err := ValidateAndNormalizeEmail(cmd.Login)
if err == nil && newEmailLogin != usr.Email {
return hs.verifyEmailUpdate(ctx, newEmailLogin, user.LoginUpdateAction, usr)
normalized, err := ValidateAndNormalizeEmail(cmd.Login)
if err == nil && usr.Email != normalized {
return hs.verifyEmailUpdate(ctx, cmd.Login, user.LoginUpdateAction, usr)
}
}
}
@@ -279,55 +275,12 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC
}
func (hs *HTTPServer) verifyEmailUpdate(ctx context.Context, email string, field user.UpdateEmailActionType, usr *user.User) response.Response {
// Verify that email is not already being used
query := user.GetUserByLoginQuery{LoginOrEmail: email}
existingUsr, err := hs.userService.GetByLogin(ctx, &query)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return response.Error(http.StatusInternalServerError, "Failed to validate if email is already in use", err)
}
if existingUsr != nil {
return response.Error(http.StatusConflict, "Email is already being used", nil)
}
// Invalidate any pending verifications for this user
expireCmd := tempuser.ExpirePreviousVerificationsCommand{InvitedByUserID: usr.ID}
err = hs.tempUserService.ExpirePreviousVerifications(ctx, &expireCmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Could not invalidate pending email verifications", err)
}
code, err := util.GetRandomString(20)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to generate random string", err)
}
tempCmd := tempuser.CreateTempUserCommand{
OrgID: -1,
if err := hs.userVerifier.VerifyEmail(ctx, user.VerifyEmailCommand{
User: *usr,
Email: email,
Code: code,
Status: tempuser.TmpUserEmailUpdateStarted,
// used to fetch the User in the second step of the verification flow
InvitedByUserID: usr.ID,
// used to determine if the user was updating their email or username in the second step of the verification flow
Name: string(field),
}
tempUser, err := hs.tempUserService.CreateTempUser(ctx, &tempCmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to create email change", err)
}
emailCmd := notifications.SendVerifyEmailCommand{Email: tempUser.Email, Code: tempUser.Code, User: usr}
err = hs.NotificationService.SendVerificationEmail(ctx, &emailCmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to send verification email", err)
}
// Record email as sent
emailSentCmd := tempuser.UpdateTempUserWithEmailSentCommand{Code: tempUser.Code}
err = hs.tempUserService.UpdateTempUserWithEmailSent(ctx, &emailSentCmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to record verification email", err)
Action: field,
}); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to generate email verification", err)
}
return response.Success("Email sent for verification")
+3
View File
@@ -397,6 +397,7 @@ func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPSer
require.NoError(t, err)
nsMock := notifications.MockNotificationService()
verifier := userimpl.ProvideVerifier(userSvc, tempUserService, nsMock)
hs := &HTTPServer{
Cfg: cfg,
@@ -404,6 +405,7 @@ func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPSer
userService: userSvc,
tempUserService: tempUserService,
NotificationService: nsMock,
userVerifier: verifier,
}
return usr, hs, nsMock
}
@@ -618,6 +620,7 @@ func TestUser_UpdateEmail(t *testing.T) {
hs.tempUserService = tempUserSvc
hs.NotificationService = nsMock
hs.SecretsService = fakes.NewFakeSecretsService()
hs.userVerifier = userimpl.ProvideVerifier(userSvc, tempUserSvc, nsMock)
// User is internal
hs.authInfoService = &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound}
})