From b2e82a4f3757feb8207b093bf9dfa0ae215af0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= Date: Thu, 18 Mar 2021 17:16:56 +0100 Subject: [PATCH] Login: Replace command dispatch by explicit call (#32088) * Fix LoginService.UpsertUser user creation * Fix API AdminCreateUser user creation * Add missing underscore import * Fix API CompleteInvite user creation * Fix API SignUpStep2 user creation --- pkg/api/admin_users.go | 7 +- pkg/api/admin_users_test.go | 99 +++---- pkg/api/api.go | 2 +- pkg/api/http_server.go | 2 +- pkg/api/org_invite.go | 7 +- pkg/api/signup.go | 7 +- pkg/server/server.go | 1 + pkg/services/login/errors.go | 9 - pkg/services/login/login.go | 256 +---------------- .../login/loginservice/loginservice.go | 269 ++++++++++++++++++ .../loginservice_test.go} | 4 +- 11 files changed, 325 insertions(+), 338 deletions(-) delete mode 100644 pkg/services/login/errors.go create mode 100644 pkg/services/login/loginservice/loginservice.go rename pkg/services/login/{login_test.go => loginservice/loginservice_test.go} (98%) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 0da29417751..01c7f7042f6 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) response.Response { +func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) response.Response { cmd := models.CreateUserCommand{ Login: form.Login, Email: form.Email, @@ -32,7 +32,8 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) respon return response.Error(400, "Password is missing or too short", nil) } - if err := bus.Dispatch(&cmd); err != nil { + user, err := hs.Login.CreateUser(cmd) + if err != nil { if errors.Is(err, models.ErrOrgNotFound) { return response.Error(400, err.Error(), nil) } @@ -46,8 +47,6 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) respon metrics.MApiAdminUserCreate.Inc() - user := cmd.Result - result := models.UserIdDTO{ Message: "User created", Id: user.Id, diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 3f229454edf..2822d8ab272 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -1,6 +1,7 @@ package api import ( + "errors" "fmt" "testing" @@ -11,14 +12,16 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/login" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( - testLogin = "test@example.com" - testPassword = "password" - nonExistingOrgID = 1000 + testLogin = "test@example.com" + testPassword = "password" + nonExistingOrgID = 1000 + existingTestLogin = "existing@example.com" ) func TestAdminAPIEndpoint(t *testing.T) { @@ -223,21 +226,6 @@ func TestAdminAPIEndpoint(t *testing.T) { adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { bus.ClearBusHandlers() - var userLogin string - var orgID int64 - - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - userLogin = cmd.Login - orgID = cmd.OrgId - - if orgID == nonExistingOrgID { - return models.ErrOrgNotFound - } - - cmd.Result = models.User{Id: testUserID} - return nil - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -245,10 +233,6 @@ func TestAdminAPIEndpoint(t *testing.T) { require.NoError(t, err) assert.Equal(t, testUserID, respJSON.Get("id").MustInt64()) assert.Equal(t, "User created", respJSON.Get("message").MustString()) - - // Verify that userLogin and orgID were transmitted correctly to the handler - assert.Equal(t, testLogin, userLogin) - assert.Equal(t, int64(0), orgID) }) }) @@ -262,21 +246,6 @@ func TestAdminAPIEndpoint(t *testing.T) { adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { bus.ClearBusHandlers() - var userLogin string - var orgID int64 - - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - userLogin = cmd.Login - orgID = cmd.OrgId - - if orgID == nonExistingOrgID { - return models.ErrOrgNotFound - } - - cmd.Result = models.User{Id: testUserID} - return nil - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -284,9 +253,6 @@ func TestAdminAPIEndpoint(t *testing.T) { require.NoError(t, err) assert.Equal(t, testUserID, respJSON.Get("id").MustInt64()) assert.Equal(t, "User created", respJSON.Get("message").MustString()) - - assert.Equal(t, testLogin, userLogin) - assert.Equal(t, testOrgID, orgID) }) }) @@ -300,47 +266,25 @@ func TestAdminAPIEndpoint(t *testing.T) { adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { bus.ClearBusHandlers() - var userLogin string - var orgID int64 - - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - userLogin = cmd.Login - orgID = cmd.OrgId - - if orgID == nonExistingOrgID { - return models.ErrOrgNotFound - } - - cmd.Result = models.User{Id: testUserID} - return nil - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 400, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, "organization not found", respJSON.Get("message").MustString()) - - assert.Equal(t, testLogin, userLogin) - assert.Equal(t, int64(1000), orgID) }) }) }) t.Run("When a server admin attempts to create a user with an already existing email/login", func(t *testing.T) { createCmd := dtos.AdminCreateUserForm{ - Login: testLogin, + Login: existingTestLogin, Password: testPassword, } adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { bus.ClearBusHandlers() - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - return models.ErrUserAlreadyExists - }) - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 412, sc.resp.Code) @@ -506,12 +450,17 @@ func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Cleanup(bus.ClearBusHandlers) + hs := HTTPServer{ + Bus: bus.GetBus(), + Login: fakeLoginService{expected: cmd}, + } + sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.context = c sc.context.UserId = testUserID - return AdminCreateUser(c, cmd) + return hs.AdminCreateUser(c, cmd) }) sc.m.Post(routePattern, sc.defaultHandler) @@ -519,3 +468,25 @@ func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern fn(sc) }) } + +type fakeLoginService struct { + login.Service + expected dtos.AdminCreateUserForm +} + +func (s fakeLoginService) CreateUser(cmd models.CreateUserCommand) (*models.User, error) { + if cmd.OrgId == nonExistingOrgID { + return nil, models.ErrOrgNotFound + } + + if cmd.Login == existingTestLogin { + return nil, models.ErrUserAlreadyExists + } + + if s.expected.Login == cmd.Login && s.expected.Email == cmd.Email && + s.expected.Password == cmd.Password && s.expected.Name == cmd.Name && s.expected.OrgId == cmd.OrgId { + return &models.User{Id: testUserID}, nil + } + + return nil, errors.New("unexpected cmd") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index e84a0a78f7e..b97b814241c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -404,7 +404,7 @@ func (hs *HTTPServer) registerRoutes() { // admin api r.Group("/api/admin", func(adminRoute routing.RouteRegister) { adminRoute.Get("/settings", routing.Wrap(AdminGetSettings)) - adminRoute.Post("/users", bind(dtos.AdminCreateUserForm{}), routing.Wrap(AdminCreateUser)) + adminRoute.Post("/users", bind(dtos.AdminCreateUserForm{}), routing.Wrap(hs.AdminCreateUser)) adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), routing.Wrap(AdminUpdateUserPassword)) adminRoute.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), routing.Wrap(AdminUpdateUserPermissions)) adminRoute.Delete("/users/:id", routing.Wrap(AdminDeleteUser)) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index ce14680964d..489fddb4dcc 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -76,7 +76,7 @@ type HTTPServer struct { QuotaService *quota.QuotaService `inject:""` RemoteCacheService *remotecache.RemoteCache `inject:""` ProvisioningService provisioning.ProvisioningService `inject:""` - Login *login.LoginService `inject:""` + Login login.Service `inject:""` License models.Licensing `inject:""` BackendPluginManager backendplugin.Manager `inject:""` DataProxy *datasourceproxy.DatasourceProxyService `inject:""` diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 815f411bd66..b10ba84ee04 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -186,7 +186,8 @@ func (hs *HTTPServer) CompleteInvite(c *models.ReqContext, completeInvite dtos.C SkipOrgSetup: true, } - if err := bus.Dispatch(&cmd); err != nil { + user, err := hs.Login.CreateUser(cmd) + if err != nil { if errors.Is(err, models.ErrUserAlreadyExists) { return response.Error(412, fmt.Sprintf("User with email '%s' or username '%s' already exists", completeInvite.Email, completeInvite.Username), err) } @@ -194,8 +195,6 @@ func (hs *HTTPServer) CompleteInvite(c *models.ReqContext, completeInvite dtos.C return response.Error(500, "failed to create user", err) } - user := &cmd.Result - if err := bus.Publish(&events.SignUpCompleted{ Name: user.NameOrFallback(), Email: user.Email, @@ -207,7 +206,7 @@ func (hs *HTTPServer) CompleteInvite(c *models.ReqContext, completeInvite dtos.C return rsp } - err := hs.loginUserWithUser(user, c) + err = hs.loginUserWithUser(user, c) if err != nil { return response.Error(500, "failed to accept invite", err) } diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 1dd14230455..4b4a237db72 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -81,8 +81,8 @@ func (hs *HTTPServer) SignUpStep2(c *models.ReqContext, form dtos.SignUpStep2For createUserCmd.EmailVerified = true } - // dispatch create command - if err := bus.Dispatch(&createUserCmd); err != nil { + user, err := hs.Login.CreateUser(createUserCmd) + if err != nil { if errors.Is(err, models.ErrUserAlreadyExists) { return response.Error(401, "User with same email address already exists", nil) } @@ -91,7 +91,6 @@ func (hs *HTTPServer) SignUpStep2(c *models.ReqContext, form dtos.SignUpStep2For } // publish signup event - user := &createUserCmd.Result if err := bus.Publish(&events.SignUpCompleted{ Email: user.Email, Name: user.NameOrFallback(), @@ -118,7 +117,7 @@ func (hs *HTTPServer) SignUpStep2(c *models.ReqContext, form dtos.SignUpStep2For apiResponse["code"] = "redirect-to-select-org" } - err := hs.loginUserWithUser(user, c) + err = hs.loginUserWithUser(user, c) if err != nil { return response.Error(500, "failed to login user", err) } diff --git a/pkg/server/server.go b/pkg/server/server.go index e9fdf261fa6..7135dbc406d 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -35,6 +35,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/auth" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/librarypanels" + _ "github.com/grafana/grafana/pkg/services/login/loginservice" _ "github.com/grafana/grafana/pkg/services/ngalert" _ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/provisioning" diff --git a/pkg/services/login/errors.go b/pkg/services/login/errors.go deleted file mode 100644 index fa5fd5faf2c..00000000000 --- a/pkg/services/login/errors.go +++ /dev/null @@ -1,9 +0,0 @@ -package login - -import "errors" - -var ( - ErrInvalidCredentials = errors.New("invalid username or password") - ErrUsersQuotaReached = errors.New("users quota reached") - ErrGettingUserQuota = errors.New("error getting user quota") -) diff --git a/pkg/services/login/login.go b/pkg/services/login/login.go index 0e4ab3da5dc..d246aed2627 100644 --- a/pkg/services/login/login.go +++ b/pkg/services/login/login.go @@ -3,261 +3,19 @@ package login import ( "errors" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/quota" - "github.com/grafana/grafana/pkg/services/sqlstore" ) -func init() { - registry.RegisterService(&LoginService{}) -} - var ( - logger = log.New("login.ext_user") + ErrInvalidCredentials = errors.New("invalid username or password") + ErrUsersQuotaReached = errors.New("users quota reached") + ErrGettingUserQuota = errors.New("error getting user quota") ) type TeamSyncFunc func(user *models.User, externalUser *models.ExternalUserInfo) error -type LoginService struct { - SQLStore *sqlstore.SQLStore `inject:""` - Bus bus.Bus `inject:""` - QuotaService *quota.QuotaService `inject:""` - TeamSync TeamSyncFunc -} - -func (ls *LoginService) Init() error { - ls.Bus.AddHandler(ls.UpsertUser) - - return nil -} - -// UpsertUser updates an existing user, or if it doesn't exist, inserts a new one. -func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error { - extUser := cmd.ExternalUser - - userQuery := &models.GetUserByAuthInfoQuery{ - AuthModule: extUser.AuthModule, - AuthId: extUser.AuthId, - UserId: extUser.UserId, - Email: extUser.Email, - Login: extUser.Login, - } - if err := bus.Dispatch(userQuery); err != nil { - if !errors.Is(err, models.ErrUserNotFound) { - return err - } - if !cmd.SignupAllowed { - log.Warnf("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule) - return ErrInvalidCredentials - } - - limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user") - if err != nil { - log.Warnf("Error getting user quota. error: %v", err) - return ErrGettingUserQuota - } - if limitReached { - return ErrUsersQuotaReached - } - - cmd.Result, err = createUser(extUser) - if err != nil { - return err - } - - if extUser.AuthModule != "" { - cmd2 := &models.SetAuthInfoCommand{ - UserId: cmd.Result.Id, - AuthModule: extUser.AuthModule, - AuthId: extUser.AuthId, - OAuthToken: extUser.OAuthToken, - } - if err := ls.Bus.Dispatch(cmd2); err != nil { - return err - } - } - } else { - cmd.Result = userQuery.Result - - err = updateUser(cmd.Result, extUser) - if err != nil { - return err - } - - // Always persist the latest token at log-in - if extUser.AuthModule != "" && extUser.OAuthToken != nil { - err = updateUserAuth(cmd.Result, extUser) - if err != nil { - return err - } - } - - if extUser.AuthModule == models.AuthModuleLDAP && userQuery.Result.IsDisabled { - // Re-enable user when it found in LDAP - if err := ls.Bus.Dispatch(&models.DisableUserCommand{UserId: cmd.Result.Id, IsDisabled: false}); err != nil { - return err - } - } - } - - if err := syncOrgRoles(cmd.Result, extUser); err != nil { - return err - } - - // Sync isGrafanaAdmin permission - if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { - if err := ls.SQLStore.UpdateUserPermissions(cmd.Result.Id, *extUser.IsGrafanaAdmin); err != nil { - return err - } - } - - if ls.TeamSync != nil { - err := ls.TeamSync(cmd.Result, extUser) - if err != nil { - return err - } - } - - return nil -} - -func createUser(extUser *models.ExternalUserInfo) (*models.User, error) { - cmd := &models.CreateUserCommand{ - Login: extUser.Login, - Email: extUser.Email, - Name: extUser.Name, - SkipOrgSetup: len(extUser.OrgRoles) > 0, - } - - if err := bus.Dispatch(cmd); err != nil { - return nil, err - } - - return &cmd.Result, nil -} - -func updateUser(user *models.User, extUser *models.ExternalUserInfo) error { - // sync user info - updateCmd := &models.UpdateUserCommand{ - UserId: user.Id, - } - - needsUpdate := false - if extUser.Login != "" && extUser.Login != user.Login { - updateCmd.Login = extUser.Login - user.Login = extUser.Login - needsUpdate = true - } - - if extUser.Email != "" && extUser.Email != user.Email { - updateCmd.Email = extUser.Email - user.Email = extUser.Email - needsUpdate = true - } - - if extUser.Name != "" && extUser.Name != user.Name { - updateCmd.Name = extUser.Name - user.Name = extUser.Name - needsUpdate = true - } - - if !needsUpdate { - return nil - } - - logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd) - return bus.Dispatch(updateCmd) -} - -func updateUserAuth(user *models.User, extUser *models.ExternalUserInfo) error { - updateCmd := &models.UpdateAuthInfoCommand{ - AuthModule: extUser.AuthModule, - AuthId: extUser.AuthId, - UserId: user.Id, - OAuthToken: extUser.OAuthToken, - } - - logger.Debug("Updating user_auth info", "user_id", user.Id) - return bus.Dispatch(updateCmd) -} - -func syncOrgRoles(user *models.User, extUser *models.ExternalUserInfo) error { - logger.Debug("Syncing organization roles", "id", user.Id, "extOrgRoles", extUser.OrgRoles) - - // don't sync org roles if none is specified - if len(extUser.OrgRoles) == 0 { - logger.Debug("Not syncing organization roles since external user doesn't have any") - return nil - } - - orgsQuery := &models.GetUserOrgListQuery{UserId: user.Id} - if err := bus.Dispatch(orgsQuery); err != nil { - return err - } - - handledOrgIds := map[int64]bool{} - deleteOrgIds := []int64{} - - // update existing org roles - for _, org := range orgsQuery.Result { - handledOrgIds[org.OrgId] = true - - extRole := extUser.OrgRoles[org.OrgId] - if extRole == "" { - deleteOrgIds = append(deleteOrgIds, org.OrgId) - } else if extRole != org.Role { - // update role - cmd := &models.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extRole} - if err := bus.Dispatch(cmd); err != nil { - return err - } - } - } - - // add any new org roles - for orgId, orgRole := range extUser.OrgRoles { - if _, exists := handledOrgIds[orgId]; exists { - continue - } - - // add role - cmd := &models.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId} - err := bus.Dispatch(cmd) - if err != nil && !errors.Is(err, models.ErrOrgNotFound) { - return err - } - } - - // delete any removed org roles - for _, orgId := range deleteOrgIds { - logger.Debug("Removing user's organization membership as part of syncing with OAuth login", - "userId", user.Id, "orgId", orgId) - cmd := &models.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id} - if err := bus.Dispatch(cmd); err != nil { - if errors.Is(err, models.ErrLastOrgAdmin) { - logger.Error(err.Error(), "userId", cmd.UserId, "orgId", cmd.OrgId) - continue - } - - return err - } - } - - // update user's default org if needed - if _, ok := extUser.OrgRoles[user.OrgId]; !ok { - for orgId := range extUser.OrgRoles { - user.OrgId = orgId - break - } - - return bus.Dispatch(&models.SetUsingOrgCommand{ - UserId: user.Id, - OrgId: user.OrgId, - }) - } - - return nil +type Service interface { + CreateUser(cmd models.CreateUserCommand) (*models.User, error) + UpsertUser(cmd *models.UpsertUserCommand) error + SetTeamSyncFunc(TeamSyncFunc) } diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go new file mode 100644 index 00000000000..917488e31c5 --- /dev/null +++ b/pkg/services/login/loginservice/loginservice.go @@ -0,0 +1,269 @@ +package loginservice + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func init() { + registry.RegisterService(&Implementation{}) +} + +var ( + logger = log.New("login.ext_user") +) + +type Implementation struct { + SQLStore *sqlstore.SQLStore `inject:""` + Bus bus.Bus `inject:""` + QuotaService *quota.QuotaService `inject:""` + TeamSync login.TeamSyncFunc +} + +func (ls *Implementation) Init() error { + ls.Bus.AddHandler(ls.UpsertUser) + + return nil +} + +// CreateUser creates inserts a new one. +func (ls *Implementation) CreateUser(cmd models.CreateUserCommand) (*models.User, error) { + return ls.SQLStore.CreateUser(context.Background(), cmd) +} + +// UpsertUser updates an existing user, or if it doesn't exist, inserts a new one. +func (ls *Implementation) UpsertUser(cmd *models.UpsertUserCommand) error { + extUser := cmd.ExternalUser + + userQuery := &models.GetUserByAuthInfoQuery{ + AuthModule: extUser.AuthModule, + AuthId: extUser.AuthId, + UserId: extUser.UserId, + Email: extUser.Email, + Login: extUser.Login, + } + if err := bus.Dispatch(userQuery); err != nil { + if !errors.Is(err, models.ErrUserNotFound) { + return err + } + if !cmd.SignupAllowed { + log.Warnf("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule) + return login.ErrInvalidCredentials + } + + limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user") + if err != nil { + log.Warnf("Error getting user quota. error: %v", err) + return login.ErrGettingUserQuota + } + if limitReached { + return login.ErrUsersQuotaReached + } + + cmd.Result, err = ls.createUser(extUser) + if err != nil { + return err + } + + if extUser.AuthModule != "" { + cmd2 := &models.SetAuthInfoCommand{ + UserId: cmd.Result.Id, + AuthModule: extUser.AuthModule, + AuthId: extUser.AuthId, + OAuthToken: extUser.OAuthToken, + } + if err := ls.Bus.Dispatch(cmd2); err != nil { + return err + } + } + } else { + cmd.Result = userQuery.Result + + err = updateUser(cmd.Result, extUser) + if err != nil { + return err + } + + // Always persist the latest token at log-in + if extUser.AuthModule != "" && extUser.OAuthToken != nil { + err = updateUserAuth(cmd.Result, extUser) + if err != nil { + return err + } + } + + if extUser.AuthModule == models.AuthModuleLDAP && userQuery.Result.IsDisabled { + // Re-enable user when it found in LDAP + if err := ls.Bus.Dispatch(&models.DisableUserCommand{UserId: cmd.Result.Id, IsDisabled: false}); err != nil { + return err + } + } + } + + if err := syncOrgRoles(cmd.Result, extUser); err != nil { + return err + } + + // Sync isGrafanaAdmin permission + if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { + if err := ls.SQLStore.UpdateUserPermissions(cmd.Result.Id, *extUser.IsGrafanaAdmin); err != nil { + return err + } + } + + if ls.TeamSync != nil { + err := ls.TeamSync(cmd.Result, extUser) + if err != nil { + return err + } + } + + return nil +} + +// SetTeamSyncFunc sets the function received through args as the team sync function. +func (ls *Implementation) SetTeamSyncFunc(teamSyncFunc login.TeamSyncFunc) { + ls.TeamSync = teamSyncFunc +} + +func (ls *Implementation) createUser(extUser *models.ExternalUserInfo) (*models.User, error) { + cmd := models.CreateUserCommand{ + Login: extUser.Login, + Email: extUser.Email, + Name: extUser.Name, + SkipOrgSetup: len(extUser.OrgRoles) > 0, + } + + return ls.CreateUser(cmd) +} + +func updateUser(user *models.User, extUser *models.ExternalUserInfo) error { + // sync user info + updateCmd := &models.UpdateUserCommand{ + UserId: user.Id, + } + + needsUpdate := false + if extUser.Login != "" && extUser.Login != user.Login { + updateCmd.Login = extUser.Login + user.Login = extUser.Login + needsUpdate = true + } + + if extUser.Email != "" && extUser.Email != user.Email { + updateCmd.Email = extUser.Email + user.Email = extUser.Email + needsUpdate = true + } + + if extUser.Name != "" && extUser.Name != user.Name { + updateCmd.Name = extUser.Name + user.Name = extUser.Name + needsUpdate = true + } + + if !needsUpdate { + return nil + } + + logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd) + return bus.Dispatch(updateCmd) +} + +func updateUserAuth(user *models.User, extUser *models.ExternalUserInfo) error { + updateCmd := &models.UpdateAuthInfoCommand{ + AuthModule: extUser.AuthModule, + AuthId: extUser.AuthId, + UserId: user.Id, + OAuthToken: extUser.OAuthToken, + } + + logger.Debug("Updating user_auth info", "user_id", user.Id) + return bus.Dispatch(updateCmd) +} + +func syncOrgRoles(user *models.User, extUser *models.ExternalUserInfo) error { + logger.Debug("Syncing organization roles", "id", user.Id, "extOrgRoles", extUser.OrgRoles) + + // don't sync org roles if none is specified + if len(extUser.OrgRoles) == 0 { + logger.Debug("Not syncing organization roles since external user doesn't have any") + return nil + } + + orgsQuery := &models.GetUserOrgListQuery{UserId: user.Id} + if err := bus.Dispatch(orgsQuery); err != nil { + return err + } + + handledOrgIds := map[int64]bool{} + deleteOrgIds := []int64{} + + // update existing org roles + for _, org := range orgsQuery.Result { + handledOrgIds[org.OrgId] = true + + extRole := extUser.OrgRoles[org.OrgId] + if extRole == "" { + deleteOrgIds = append(deleteOrgIds, org.OrgId) + } else if extRole != org.Role { + // update role + cmd := &models.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extRole} + if err := bus.Dispatch(cmd); err != nil { + return err + } + } + } + + // add any new org roles + for orgId, orgRole := range extUser.OrgRoles { + if _, exists := handledOrgIds[orgId]; exists { + continue + } + + // add role + cmd := &models.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId} + err := bus.Dispatch(cmd) + if err != nil && !errors.Is(err, models.ErrOrgNotFound) { + return err + } + } + + // delete any removed org roles + for _, orgId := range deleteOrgIds { + logger.Debug("Removing user's organization membership as part of syncing with OAuth login", + "userId", user.Id, "orgId", orgId) + cmd := &models.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id} + if err := bus.Dispatch(cmd); err != nil { + if errors.Is(err, models.ErrLastOrgAdmin) { + logger.Error(err.Error(), "userId", cmd.UserId, "orgId", cmd.OrgId) + continue + } + + return err + } + } + + // update user's default org if needed + if _, ok := extUser.OrgRoles[user.OrgId]; !ok { + for orgId := range extUser.OrgRoles { + user.OrgId = orgId + break + } + + return bus.Dispatch(&models.SetUsingOrgCommand{ + UserId: user.Id, + OrgId: user.OrgId, + }) + } + + return nil +} diff --git a/pkg/services/login/login_test.go b/pkg/services/login/loginservice/loginservice_test.go similarity index 98% rename from pkg/services/login/login_test.go rename to pkg/services/login/loginservice/loginservice_test.go index 04953b567a1..307a9d490fd 100644 --- a/pkg/services/login/login_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -1,4 +1,4 @@ -package login +package loginservice import ( "errors" @@ -77,7 +77,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { } func Test_teamSync(t *testing.T) { - login := LoginService{ + login := Implementation{ Bus: bus.New(), QuotaService: "a.QuotaService{}, }