Auth: Fix email verification bypass when using basic authentication (#83484)
This commit is contained in:
@@ -102,6 +102,7 @@ func (srv *CleanUpService) clean(ctx context.Context) {
|
||||
{"expire old user invites", srv.expireOldUserInvites},
|
||||
{"delete stale short URLs", srv.deleteStaleShortURLs},
|
||||
{"delete stale query history", srv.deleteStaleQueryHistory},
|
||||
{"expire old email verifications", srv.expireOldVerifications},
|
||||
}
|
||||
|
||||
logger := srv.log.FromContext(ctx)
|
||||
@@ -237,6 +238,21 @@ func (srv *CleanUpService) expireOldUserInvites(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *CleanUpService) expireOldVerifications(ctx context.Context) {
|
||||
logger := srv.log.FromContext(ctx)
|
||||
maxVerificationLifetime := srv.Cfg.VerificationEmailMaxLifetime
|
||||
|
||||
cmd := tempuser.ExpireTempUsersCommand{
|
||||
OlderThan: time.Now().Add(-maxVerificationLifetime),
|
||||
}
|
||||
|
||||
if err := srv.tempUserService.ExpireOldVerifications(ctx, &cmd); err != nil {
|
||||
logger.Error("Problem expiring email verifications", "error", err.Error())
|
||||
} else {
|
||||
logger.Debug("Expired email verifications", "rows affected", cmd.NumExpired)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *CleanUpService) deleteStaleShortURLs(ctx context.Context) {
|
||||
logger := srv.log.FromContext(ctx)
|
||||
cmd := shorturls.DeleteShortUrlCommand{
|
||||
|
||||
@@ -2,13 +2,17 @@ package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type NotificationServiceMock struct {
|
||||
Webhook SendWebhookSync
|
||||
EmailSync SendEmailCommandSync
|
||||
Email SendEmailCommand
|
||||
ShouldError error
|
||||
Webhook SendWebhookSync
|
||||
EmailSync SendEmailCommandSync
|
||||
Email SendEmailCommand
|
||||
EmailVerified bool
|
||||
EmailVerification SendVerifyEmailCommand
|
||||
ShouldError error
|
||||
|
||||
WebhookHandler func(context.Context, *SendWebhookSync) error
|
||||
EmailHandlerSync func(context.Context, *SendEmailCommandSync) error
|
||||
@@ -39,4 +43,20 @@ func (ns *NotificationServiceMock) SendEmailCommandHandler(ctx context.Context,
|
||||
return ns.ShouldError
|
||||
}
|
||||
|
||||
func (ns *NotificationServiceMock) SendResetPasswordEmail(ctx context.Context, cmd *SendResetPasswordEmailCommand) error {
|
||||
// TODO: Implement if needed
|
||||
return ns.ShouldError
|
||||
}
|
||||
|
||||
func (ns *NotificationServiceMock) ValidateResetPasswordCode(ctx context.Context, query *ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) (*user.User, error) {
|
||||
// TODO: Implement if needed
|
||||
return nil, ns.ShouldError
|
||||
}
|
||||
|
||||
func (ns *NotificationServiceMock) SendVerificationEmail(ctx context.Context, cmd *SendVerifyEmailCommand) error {
|
||||
ns.EmailVerified = true
|
||||
ns.EmailVerification = *cmd
|
||||
return ns.ShouldError
|
||||
}
|
||||
|
||||
func MockNotificationService() *NotificationServiceMock { return &NotificationServiceMock{} }
|
||||
|
||||
@@ -51,3 +51,9 @@ type SendResetPasswordEmailCommand struct {
|
||||
type ValidateResetPasswordCodeQuery struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
type SendVerifyEmailCommand struct {
|
||||
User *user.User
|
||||
Code string
|
||||
Email string
|
||||
}
|
||||
|
||||
@@ -28,15 +28,25 @@ type EmailSender interface {
|
||||
SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error
|
||||
SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error
|
||||
}
|
||||
type PasswordResetMailer interface {
|
||||
SendResetPasswordEmail(ctx context.Context, cmd *SendResetPasswordEmailCommand) error
|
||||
ValidateResetPasswordCode(ctx context.Context, query *ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) (*user.User, error)
|
||||
}
|
||||
type EmailVerificationMailer interface {
|
||||
SendVerificationEmail(ctx context.Context, cmd *SendVerifyEmailCommand) error
|
||||
}
|
||||
type Service interface {
|
||||
WebhookSender
|
||||
EmailSender
|
||||
PasswordResetMailer
|
||||
EmailVerificationMailer
|
||||
}
|
||||
|
||||
var mailTemplates *template.Template
|
||||
var tmplResetPassword = "reset_password"
|
||||
var tmplSignUpStarted = "signup_started"
|
||||
var tmplWelcomeOnSignUp = "welcome_on_signup"
|
||||
var tmplVerifyEmail = "verify_email_update"
|
||||
|
||||
func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer, store TempUserStore) (*NotificationService, error) {
|
||||
ns := &NotificationService{
|
||||
@@ -257,6 +267,20 @@ func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, qu
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (ns *NotificationService) SendVerificationEmail(ctx context.Context, cmd *SendVerifyEmailCommand) error {
|
||||
return ns.SendEmailCommandHandlerSync(ctx, &SendEmailCommandSync{
|
||||
SendEmailCommand: SendEmailCommand{
|
||||
To: []string{cmd.Email},
|
||||
Template: tmplVerifyEmail,
|
||||
Data: map[string]any{
|
||||
"Code": url.QueryEscape(cmd.Code),
|
||||
"Name": cmd.User.Name,
|
||||
"VerificationEmailLifetimeHours": int(ns.Cfg.VerificationEmailMaxLifetime.Hours()),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ns *NotificationService) signUpStartedHandler(ctx context.Context, evt *events.SignUpStarted) error {
|
||||
if !setting.VerifyEmailEnabled {
|
||||
return nil
|
||||
|
||||
@@ -15,11 +15,14 @@ var (
|
||||
type TempUserStatus string
|
||||
|
||||
const (
|
||||
TmpUserSignUpStarted TempUserStatus = "SignUpStarted"
|
||||
TmpUserInvitePending TempUserStatus = "InvitePending"
|
||||
TmpUserCompleted TempUserStatus = "Completed"
|
||||
TmpUserRevoked TempUserStatus = "Revoked"
|
||||
TmpUserExpired TempUserStatus = "Expired"
|
||||
TmpUserSignUpStarted TempUserStatus = "SignUpStarted"
|
||||
TmpUserInvitePending TempUserStatus = "InvitePending"
|
||||
TmpUserCompleted TempUserStatus = "Completed"
|
||||
TmpUserRevoked TempUserStatus = "Revoked"
|
||||
TmpUserExpired TempUserStatus = "Expired"
|
||||
TmpUserEmailUpdateStarted TempUserStatus = "EmailUpdateStarted"
|
||||
TmpUserEmailUpdateCompleted TempUserStatus = "EmailUpdateCompleted"
|
||||
TmpUserEmailUpdateExpired TempUserStatus = "EmailUpdateExpired"
|
||||
)
|
||||
|
||||
// TempUser holds data for org invites and unconfirmed sign ups
|
||||
@@ -67,6 +70,12 @@ type ExpireTempUsersCommand struct {
|
||||
NumExpired int64
|
||||
}
|
||||
|
||||
type ExpirePreviousVerificationsCommand struct {
|
||||
InvitedByUserID int64
|
||||
|
||||
NumExpired int64
|
||||
}
|
||||
|
||||
type UpdateTempUserWithEmailSentCommand struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ type Service interface {
|
||||
GetTempUsersQuery(ctx context.Context, query *GetTempUsersQuery) ([]*TempUserDTO, error)
|
||||
GetTempUserByCode(ctx context.Context, query *GetTempUserByCodeQuery) (*TempUserDTO, error)
|
||||
ExpireOldUserInvites(ctx context.Context, cmd *ExpireTempUsersCommand) error
|
||||
ExpireOldVerifications(ctx context.Context, cmd *ExpireTempUsersCommand) error
|
||||
ExpirePreviousVerifications(ctx context.Context, cmd *ExpirePreviousVerificationsCommand) error
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type store interface {
|
||||
GetTempUsersQuery(ctx context.Context, query *tempuser.GetTempUsersQuery) ([]*tempuser.TempUserDTO, error)
|
||||
GetTempUserByCode(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error)
|
||||
ExpireOldUserInvites(ctx context.Context, cmd *tempuser.ExpireTempUsersCommand) error
|
||||
ExpireOldVerifications(ctx context.Context, cmd *tempuser.ExpireTempUsersCommand) error
|
||||
ExpirePreviousVerifications(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error
|
||||
}
|
||||
|
||||
type xormStore struct {
|
||||
@@ -175,3 +177,27 @@ func (ss *xormStore) ExpireOldUserInvites(ctx context.Context, cmd *tempuser.Exp
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *xormStore) ExpireOldVerifications(ctx context.Context, cmd *tempuser.ExpireTempUsersCommand) error {
|
||||
return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
var rawSQL = "UPDATE temp_user SET status = ?, updated = ? WHERE created <= ? AND status = ?"
|
||||
if result, err := sess.Exec(rawSQL, string(tempuser.TmpUserEmailUpdateExpired), time.Now().Unix(), cmd.OlderThan.Unix(), string(tempuser.TmpUserEmailUpdateStarted)); err != nil {
|
||||
return err
|
||||
} else if cmd.NumExpired, err = result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *xormStore) ExpirePreviousVerifications(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error {
|
||||
return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
var rawSQL = "UPDATE temp_user SET status = ?, updated = ? WHERE invited_by_user_id = ? AND status = ?"
|
||||
if result, err := sess.Exec(rawSQL, string(tempuser.TmpUserEmailUpdateExpired), time.Now().Unix(), cmd.InvitedByUserID, string(tempuser.TmpUserEmailUpdateStarted)); err != nil {
|
||||
return err
|
||||
} else if cmd.NumExpired, err = result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,7 +112,32 @@ func TestIntegrationTempUserCommandsAndQueries(t *testing.T) {
|
||||
require.False(t, queryResult[0].EmailSentOn.UTC().Before(queryResult[0].Created.UTC()))
|
||||
})
|
||||
|
||||
t.Run("Should be able expire temp user", func(t *testing.T) {
|
||||
t.Run("Should be able expire all pending verifications from a user", func(t *testing.T) {
|
||||
userID := int64(99)
|
||||
verifications := 5
|
||||
cmd := tempuser.CreateTempUserCommand{
|
||||
OrgID: -1,
|
||||
Name: "email-update",
|
||||
Code: "asd",
|
||||
Email: "e@as.co",
|
||||
Status: tempuser.TmpUserEmailUpdateStarted,
|
||||
InvitedByUserID: userID,
|
||||
}
|
||||
db := db.InitTestDB(t)
|
||||
store = &xormStore{db: db, cfg: db.Cfg}
|
||||
|
||||
for i := 0; i < verifications; i++ {
|
||||
tempUser, err = store.CreateTempUser(context.Background(), &cmd)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
cmd2 := tempuser.ExpirePreviousVerificationsCommand{InvitedByUserID: userID}
|
||||
err := store.ExpirePreviousVerifications(context.Background(), &cmd2)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(verifications), cmd2.NumExpired)
|
||||
})
|
||||
|
||||
t.Run("Should be able expire temp user related to org invite", func(t *testing.T) {
|
||||
setup(t)
|
||||
createdAt := time.Unix(tempUser.Created, 0)
|
||||
cmd2 := tempuser.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
|
||||
@@ -128,4 +153,34 @@ func TestIntegrationTempUserCommandsAndQueries(t *testing.T) {
|
||||
require.Equal(t, int64(0), cmd2.NumExpired)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should be able expire temp user related to email verification", func(t *testing.T) {
|
||||
cmd := tempuser.CreateTempUserCommand{
|
||||
OrgID: 2256,
|
||||
Name: "email-update",
|
||||
Code: "asd",
|
||||
Email: "e@as.co",
|
||||
Status: tempuser.TmpUserEmailUpdateStarted,
|
||||
InvitedByUserID: 99,
|
||||
}
|
||||
db := db.InitTestDB(t)
|
||||
store = &xormStore{db: db, cfg: db.Cfg}
|
||||
|
||||
tempUser, err = store.CreateTempUser(context.Background(), &cmd)
|
||||
require.Nil(t, err)
|
||||
|
||||
createdAt := time.Unix(tempUser.Created, 0)
|
||||
cmd2 := tempuser.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
|
||||
err := store.ExpireOldVerifications(context.Background(), &cmd2)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(1), cmd2.NumExpired)
|
||||
|
||||
t.Run("Should do nothing when no temp users to expire", func(t *testing.T) {
|
||||
createdAt := time.Unix(tempUser.Created, 0)
|
||||
cmd2 := tempuser.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
|
||||
err := store.ExpireOldVerifications(context.Background(), &cmd2)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(0), cmd2.NumExpired)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,3 +68,19 @@ func (s *Service) ExpireOldUserInvites(ctx context.Context, cmd *tempuser.Expire
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ExpireOldVerifications(ctx context.Context, cmd *tempuser.ExpireTempUsersCommand) error {
|
||||
err := s.store.ExpireOldVerifications(ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ExpirePreviousVerifications(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error {
|
||||
err := s.store.ExpirePreviousVerifications(ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ const (
|
||||
HelpFlagDashboardHelp1
|
||||
)
|
||||
|
||||
type UpdateEmailActionType string
|
||||
|
||||
const (
|
||||
EmailUpdateAction UpdateEmailActionType = "email-update"
|
||||
LoginUpdateAction UpdateEmailActionType = "login-update"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
Version int
|
||||
|
||||
Reference in New Issue
Block a user