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
parent 38a8bf10f3
commit 8d9521fb6d
13 changed files with 267 additions and 64 deletions

View File

@@ -18,6 +18,7 @@ var (
)
var (
ErrEmailConflict = errutil.Conflict("user.email-conflict", errutil.WithPublicMessage("Email is already being used"))
ErrEmptyUsernameAndEmail = errutil.BadRequest(
"user.empty-username-and-email", errutil.WithPublicMessage("Need to specify either username or email"),
)

View File

@@ -220,6 +220,12 @@ type GetUserByIDQuery struct {
ID int64
}
type VerifyEmailCommand struct {
User User
Email string
Action UpdateEmailActionType
}
type ErrCaseInsensitiveLoginConflict struct {
Users []User
}

View File

@@ -29,3 +29,7 @@ type Service interface {
SetUserHelpFlag(context.Context, *SetUserHelpFlagCommand) error
GetProfile(context.Context, *GetUserProfileQuery) (*UserProfileDTO, error)
}
type Verifier interface {
VerifyEmail(ctx context.Context, cmd VerifyEmailCommand) error
}

View File

@@ -0,0 +1,82 @@
package userimpl
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/services/notifications"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
)
var _ user.Verifier = (*Verifier)(nil)
func ProvideVerifier(us user.Service, ts tempuser.Service, ns notifications.Service) *Verifier {
return &Verifier{us, ts, ns}
}
type Verifier struct {
us user.Service
ts tempuser.Service
ns notifications.Service
}
func (s *Verifier) VerifyEmail(ctx context.Context, cmd user.VerifyEmailCommand) error {
usr, err := s.us.GetByLogin(ctx, &user.GetUserByLoginQuery{
LoginOrEmail: cmd.Email,
})
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return err
}
// if email is already used by another user we stop here
if usr != nil && usr.ID != cmd.User.ID {
return user.ErrEmailConflict.Errorf("email already used")
}
code, err := util.GetRandomString(20)
if err != nil {
return fmt.Errorf("failed to generate verification code: %w", err)
}
// invalidate any pending verifications for user
if err = s.ts.ExpirePreviousVerifications(
ctx, &tempuser.ExpirePreviousVerificationsCommand{InvitedByUserID: cmd.User.ID},
); err != nil {
return fmt.Errorf("failed to expire previous verifications: %w", err)
}
tmpUsr, err := s.ts.CreateTempUser(ctx, &tempuser.CreateTempUserCommand{
OrgID: -1,
// used to determine if the user was updating their email or username in the second step of the verification flow
Name: string(cmd.Action),
// used to fetch the User in the second step of the verification flow
InvitedByUserID: cmd.User.ID,
Email: cmd.Email,
Code: code,
Status: tempuser.TmpUserEmailUpdateStarted,
})
if err != nil {
return fmt.Errorf("failed to generate temp user for email verification: %w", err)
}
if err := s.ns.SendVerificationEmail(ctx, &notifications.SendVerifyEmailCommand{
User: &cmd.User,
Code: tmpUsr.Code,
Email: cmd.Email,
}); err != nil {
return fmt.Errorf("failed to send verification email: %w", err)
}
if err := s.ts.UpdateTempUserWithEmailSent(ctx, &tempuser.UpdateTempUserWithEmailSentCommand{
Code: tmpUsr.Code,
}); err != nil {
return fmt.Errorf("failed to mark email as sent: %w", err)
}
return nil
}

View File

@@ -0,0 +1,108 @@
package userimpl
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/services/notifications"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/temp_user/tempusertest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
)
func TestVerifier_VerifyEmail(t *testing.T) {
ts := &tempusertest.FakeTempUserService{}
us := &usertest.FakeUserService{}
ns := notifications.MockNotificationService()
type calls struct {
expireCalled bool
createCalled bool
updateCalled bool
}
verifier := ProvideVerifier(us, ts, ns)
t.Run("should error if email already exist for other user", func(t *testing.T) {
us.ExpectedUser = &user.User{ID: 1}
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{
User: user.User{ID: 2},
Email: "some@email.com",
Action: user.EmailUpdateAction,
})
assert.ErrorIs(t, err, user.ErrEmailConflict)
})
t.Run("should succeed when no user has the email", func(t *testing.T) {
us.ExpectedUser = nil
var c calls
ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error {
c.expireCalled = true
return nil
}
ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) {
c.createCalled = true
return &tempuser.TempUser{
OrgID: cmd.OrgID,
Email: cmd.Email,
Name: cmd.Name,
InvitedByUserID: cmd.InvitedByUserID,
Code: cmd.Code,
}, nil
}
ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error {
c.updateCalled = true
return nil
}
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{
User: user.User{ID: 2},
Email: "some@email.com",
Action: user.EmailUpdateAction,
})
assert.ErrorIs(t, err, nil)
assert.True(t, c.expireCalled)
assert.True(t, c.createCalled)
assert.True(t, c.updateCalled)
})
t.Run("should succeed when the user holding the email is the same user that want to verify it", func(t *testing.T) {
us.ExpectedUser = &user.User{ID: 2}
var c calls
ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error {
c.expireCalled = true
return nil
}
ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) {
c.createCalled = true
return &tempuser.TempUser{
OrgID: cmd.OrgID,
Email: cmd.Email,
Name: cmd.Name,
InvitedByUserID: cmd.InvitedByUserID,
Code: cmd.Code,
}, nil
}
ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error {
c.updateCalled = true
return nil
}
err := verifier.VerifyEmail(context.Background(), user.VerifyEmailCommand{
User: user.User{ID: 2},
Email: "some@email.com",
Action: user.EmailUpdateAction,
})
assert.ErrorIs(t, err, nil)
assert.True(t, c.expireCalled)
assert.True(t, c.createCalled)
assert.True(t, c.updateCalled)
})
}