mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
dfd4eccc7c
commit
b2e82a4f37
@ -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,
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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:""`
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
)
|
@ -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)
|
||||
}
|
||||
|
269
pkg/services/login/loginservice/loginservice.go
Normal file
269
pkg/services/login/loginservice/loginservice.go
Normal file
@ -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
|
||||
}
|
@ -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{},
|
||||
}
|
Loading…
Reference in New Issue
Block a user