Login: remove login.Service (#73542)

This commit is contained in:
Karl Persson 2023-08-21 13:15:31 +02:00 committed by GitHub
parent ab587b6884
commit 618daf0518
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 9 additions and 638 deletions

View File

@ -77,7 +77,6 @@ import (
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
"github.com/grafana/grafana/pkg/services/navtree/navtreeimpl"
@ -215,8 +214,6 @@ var wireBasicSet = wire.NewSet(
quotaimpl.ProvideService,
remotecache.ProvideService,
wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)),
loginservice.ProvideService,
wire.Bind(new(login.Service), new(*loginservice.Implementation)),
authinfoservice.ProvideAuthInfoService,
wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)),
authinfodatabase.ProvideAuthInfoStore,

View File

@ -46,6 +46,12 @@ var (
)
)
var (
errUsersQuotaReached = errors.New("users quota reached")
errGettingUserQuota = errors.New("error getting user quota")
errSignupNotAllowed = errors.New("system administrator has disabled signup")
)
func ProvideUserSync(userService user.Service,
userProtectionService login.UserProtectionService,
authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync {
@ -82,7 +88,7 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth
if errors.Is(errUserInDB, user.ErrUserNotFound) {
if !id.ClientParams.AllowSignUp {
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errUserSignupDisabled.Errorf("%w", login.ErrSignupNotAllowed)
return errUserSignupDisabled.Errorf("%w", errSignupNotAllowed)
}
// create user
@ -259,10 +265,10 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us
limitReached, errLimit := s.quotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil)
if errLimit != nil {
s.log.FromContext(ctx).Error("Failed to check quota", "error", errLimit)
return nil, errSyncUserInternal.Errorf("%w", login.ErrGettingUserQuota)
return nil, errSyncUserInternal.Errorf("%w", errGettingUserQuota)
}
if limitReached {
return nil, errSyncUserForbidden.Errorf("%w", login.ErrUsersQuotaReached)
return nil, errSyncUserForbidden.Errorf("%w", errUsersQuotaReached)
}
}

View File

@ -1,23 +0,0 @@
package login
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/services/user"
)
var (
ErrInvalidCredentials = errors.New("invalid username or password")
ErrUsersQuotaReached = errors.New("users quota reached")
ErrGettingUserQuota = errors.New("error getting user quota")
ErrSignupNotAllowed = errors.New("system administrator has disabled signup")
)
type TeamSyncFunc func(user *user.User, externalUser *ExternalUserInfo) error
type Service interface {
UpsertUser(ctx context.Context, cmd *UpsertUserCommand) (*user.User, error)
DisableExternalUser(ctx context.Context, username string) error
SetTeamSyncFunc(TeamSyncFunc)
}

View File

@ -1,342 +0,0 @@
package loginservice
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
)
var (
logger = log.New("login.ext_user")
)
func ProvideService(
userService user.Service,
quotaService quota.Service,
authInfoService login.AuthInfoService,
accessControl accesscontrol.Service,
orgService org.Service,
) *Implementation {
s := &Implementation{
userService: userService,
QuotaService: quotaService,
AuthInfoService: authInfoService,
accessControl: accessControl,
orgService: orgService,
}
return s
}
type Implementation struct {
userService user.Service
AuthInfoService login.AuthInfoService
QuotaService quota.Service
TeamSync login.TeamSyncFunc
accessControl accesscontrol.Service
orgService org.Service
}
// UpsertUser updates an existing user, or if it doesn't exist, inserts a new one.
func (ls *Implementation) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) (result *user.User, err error) {
var logger log.Logger = logger
if cmd.ReqContext != nil && cmd.ReqContext.Logger != nil {
logger = cmd.ReqContext.Logger
}
extUser := cmd.ExternalUser
usr, errAuthLookup := ls.AuthInfoService.LookupAndUpdate(ctx, &login.GetUserByAuthInfoQuery{
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
UserLookupParams: cmd.UserLookupParams,
})
if errAuthLookup != nil {
if !errors.Is(errAuthLookup, user.ErrUserNotFound) {
return nil, errAuthLookup
}
if !cmd.SignupAllowed {
logger.Warn("Not allowing login, user not found in internal user database and allow signup = false", "authmode", extUser.AuthModule)
return nil, login.ErrSignupNotAllowed
}
// quota check (FIXME: (jguer) this should be done in the user service)
// we may insert in both user and org_user tables
// therefore we need to query check quota for both user and org services
for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} {
limitReached, errLimit := ls.QuotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil)
if errLimit != nil {
logger.Warn("Error getting user quota.", "error", errLimit)
return nil, login.ErrGettingUserQuota
}
if limitReached {
return nil, login.ErrUsersQuotaReached
}
}
createdUser, errCreateUser := ls.userService.Create(ctx, &user.CreateUserCommand{
Login: extUser.Login,
Email: extUser.Email,
Name: extUser.Name,
SkipOrgSetup: len(extUser.OrgRoles) > 0,
})
if errCreateUser != nil {
return nil, errCreateUser
}
result = &user.User{
ID: createdUser.ID,
Version: createdUser.Version,
Email: createdUser.Email,
Name: createdUser.Name,
Login: createdUser.Login,
Password: createdUser.Password,
Salt: createdUser.Salt,
Rands: createdUser.Rands,
Company: createdUser.Company,
EmailVerified: createdUser.EmailVerified,
Theme: createdUser.Theme,
HelpFlags1: createdUser.HelpFlags1,
IsDisabled: createdUser.IsDisabled,
IsAdmin: createdUser.IsAdmin,
IsServiceAccount: createdUser.IsServiceAccount,
OrgID: createdUser.OrgID,
Created: createdUser.Created,
Updated: createdUser.Updated,
LastSeenAt: createdUser.LastSeenAt,
}
if extUser.AuthModule != "" {
cmd2 := &login.SetAuthInfoCommand{
UserId: result.ID,
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
OAuthToken: extUser.OAuthToken,
}
if errSetAuth := ls.AuthInfoService.SetAuthInfo(ctx, cmd2); errSetAuth != nil {
return nil, errSetAuth
}
}
} else {
result = usr
if errUserMod := ls.updateUser(ctx, result, extUser); errUserMod != nil {
return nil, errUserMod
}
// Always persist the latest token at log-in
if extUser.AuthModule != "" && extUser.OAuthToken != nil {
if errAuthMod := ls.updateUserAuth(ctx, result, extUser); errAuthMod != nil {
return nil, errAuthMod
}
}
if extUser.AuthModule == login.LDAPAuthModule && usr.IsDisabled {
// Re-enable user when it found in LDAP
if errDisableUser := ls.userService.Disable(ctx,
&user.DisableUserCommand{
UserID: result.ID, IsDisabled: false}); errDisableUser != nil {
return nil, errDisableUser
}
}
}
if errSyncRole := ls.syncOrgRoles(ctx, result, extUser); errSyncRole != nil {
return nil, errSyncRole
}
// Sync isGrafanaAdmin permission
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != result.IsAdmin {
if errPerms := ls.userService.UpdatePermissions(ctx, result.ID, *extUser.IsGrafanaAdmin); errPerms != nil {
return nil, errPerms
}
}
// There are external providers where we want to completely skip team synchronization see - https://github.com/grafana/grafana/issues/62175
if ls.TeamSync != nil && !extUser.SkipTeamSync {
if errTeamSync := ls.TeamSync(result, extUser); errTeamSync != nil {
return nil, errTeamSync
}
}
return result, nil
}
func (ls *Implementation) DisableExternalUser(ctx context.Context, username string) error {
// Check if external user exist in Grafana
userQuery := &login.GetExternalUserInfoByLoginQuery{
LoginOrEmail: username,
}
userInfo, err := ls.AuthInfoService.GetExternalUserInfoByLogin(ctx, userQuery)
if err != nil {
return err
}
if userInfo.IsDisabled {
return nil
}
logger.Debug(
"Disabling external user",
"user",
userInfo.Login,
)
// Mark user as disabled in grafana db
disableUserCmd := &user.DisableUserCommand{
UserID: userInfo.UserId,
IsDisabled: true,
}
if err := ls.userService.Disable(ctx, disableUserCmd); err != nil {
logger.Debug(
"Error disabling external user",
"user",
userInfo.Login,
"message",
err.Error(),
)
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) updateUser(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error {
// sync user info
updateCmd := &user.UpdateUserCommand{
UserID: usr.ID,
}
needsUpdate := false
if extUser.Login != "" && extUser.Login != usr.Login {
updateCmd.Login = extUser.Login
usr.Login = extUser.Login
needsUpdate = true
}
if extUser.Email != "" && extUser.Email != usr.Email {
updateCmd.Email = extUser.Email
usr.Email = extUser.Email
needsUpdate = true
}
if extUser.Name != "" && extUser.Name != usr.Name {
updateCmd.Name = extUser.Name
usr.Name = extUser.Name
needsUpdate = true
}
if !needsUpdate {
return nil
}
logger.Debug("Syncing user info", "id", usr.ID, "update", updateCmd)
return ls.userService.Update(ctx, updateCmd)
}
func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, extUser *login.ExternalUserInfo) error {
updateCmd := &login.UpdateAuthInfoCommand{
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
UserId: user.ID,
OAuthToken: extUser.OAuthToken,
}
logger.Debug("Updating user_auth info", "user_id", user.ID)
return ls.AuthInfoService.UpdateAuthInfo(ctx, updateCmd)
}
func (ls *Implementation) syncOrgRoles(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error {
logger.Debug("Syncing organization roles", "id", usr.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 := &org.GetUserOrgListQuery{UserID: usr.ID}
result, err := ls.orgService.GetUserOrgList(ctx, orgsQuery)
if err != nil {
return err
}
handledOrgIds := map[int64]bool{}
deleteOrgIds := []int64{}
// update existing org roles
for _, orga := range result {
handledOrgIds[orga.OrgID] = true
extRole := extUser.OrgRoles[orga.OrgID]
if extRole == "" {
deleteOrgIds = append(deleteOrgIds, orga.OrgID)
} else if extRole != orga.Role {
// update role
cmd := &org.UpdateOrgUserCommand{OrgID: orga.OrgID, UserID: usr.ID, Role: extRole}
if err := ls.orgService.UpdateOrgUser(ctx, 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 := &org.AddOrgUserCommand{UserID: usr.ID, Role: orgRole, OrgID: orgId}
err := ls.orgService.AddOrgUser(ctx, cmd)
if err != nil && !errors.Is(err, org.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", usr.ID, "orgId", orgId)
cmd := &org.RemoveOrgUserCommand{OrgID: orgId, UserID: usr.ID}
if err := ls.orgService.RemoveOrgUser(ctx, cmd); err != nil {
if errors.Is(err, org.ErrLastOrgAdmin) {
logger.Error(err.Error(), "userId", cmd.UserID, "orgId", cmd.OrgID)
continue
}
return err
}
if err := ls.accessControl.DeleteUserPermissions(ctx, orgId, cmd.UserID); err != nil {
logger.Warn("failed to delete permissions for user", "error", err, "userID", cmd.UserID, "orgID", orgId)
}
}
// update user's default org if needed
if _, ok := extUser.OrgRoles[usr.OrgID]; !ok {
for orgId := range extUser.OrgRoles {
usr.OrgID = orgId
break
}
return ls.userService.SetUsingOrg(ctx, &user.SetUsingOrgCommand{
UserID: usr.ID,
OrgID: usr.OrgID,
})
}
return nil
}

View File

@ -1,26 +0,0 @@
package loginservice
import (
"context"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
)
type LoginServiceMock struct {
login.Service
ExpectedUser *user.User
ExpectedUserFunc func(cmd *login.UpsertUserCommand) *user.User
ExpectedError error
}
func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) (*user.User, error) {
if s.ExpectedUserFunc != nil {
return s.ExpectedUserFunc(cmd), s.ExpectedError
}
return s.ExpectedUser, s.ExpectedError
}
func (s LoginServiceMock) DisableExternalUser(ctx context.Context, username string) error {
return nil
}

View File

@ -1,214 +0,0 @@
package loginservice
import (
"bytes"
"context"
"errors"
"testing"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
)
func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) {
user := createSimpleUser()
externalUser := createSimpleExternalUser()
authInfoMock := &logintest.AuthInfoServiceFake{}
login := Implementation{
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
userService: usertest.NewUserServiceFake(),
orgService: orgtest.NewOrgServiceFake(),
}
err := login.syncOrgRoles(context.Background(), &user, &externalUser)
require.NoError(t, err)
}
func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
buf := &bytes.Buffer{}
logger.Swap(level.NewFilter(log.NewLogfmtLogger(buf), level.AllowInfo()))
user := createSimpleUser()
externalUser := createSimpleExternalUser()
authInfoMock := &logintest.AuthInfoServiceFake{}
orgService := orgtest.NewOrgServiceFake()
orgService.ExpectedUserOrgDTO = createUserOrgDTO()
orgService.ExpectedOrgListResponse = createResponseWithOneErrLastOrgAdminItem()
login := Implementation{
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
userService: usertest.NewUserServiceFake(),
orgService: orgService,
accessControl: &actest.FakeService{},
}
err := login.syncOrgRoles(context.Background(), &user, &externalUser)
require.NoError(t, err)
assert.Contains(t, buf.String(), org.ErrLastOrgAdmin.Error())
}
func Test_teamSync(t *testing.T) {
authInfoMock := &logintest.AuthInfoServiceFake{}
loginsvc := Implementation{
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
}
email := "test_user@example.org"
upsertCmd := &login.UpsertUserCommand{ExternalUser: &login.ExternalUserInfo{Email: email},
UserLookupParams: login.UserLookupParams{Email: &email}}
expectedUser := &user.User{
ID: 1,
Email: email,
Name: "test_user",
Login: "test_user",
}
authInfoMock.ExpectedUser = expectedUser
var actualUser *user.User
var actualExternalUser *login.ExternalUserInfo
t.Run("login.TeamSync should not be called when nil", func(t *testing.T) {
_, err := loginsvc.UpsertUser(context.Background(), upsertCmd)
require.Nil(t, err)
assert.Nil(t, actualUser)
assert.Nil(t, actualExternalUser)
t.Run("login.TeamSync should be called when not nil", func(t *testing.T) {
teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error {
actualUser = user
actualExternalUser = externalUser
return nil
}
loginsvc.TeamSync = teamSyncFunc
_, err := loginsvc.UpsertUser(context.Background(), upsertCmd)
require.Nil(t, err)
assert.Equal(t, actualUser, expectedUser)
assert.Equal(t, actualExternalUser, upsertCmd.ExternalUser)
})
t.Run("login.TeamSync should not be called when not nil and skipTeamSync is set for externalUserInfo", func(t *testing.T) {
var actualUser *user.User
var actualExternalUser *login.ExternalUserInfo
upsertCmdSkipTeamSync := &login.UpsertUserCommand{
ExternalUser: &login.ExternalUserInfo{
Email: email,
// sending in ExternalUserInfo with SkipTeamSync yields no team sync
SkipTeamSync: true,
},
UserLookupParams: login.UserLookupParams{Email: &email},
}
teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error {
actualUser = user
actualExternalUser = externalUser
return nil
}
loginsvc.TeamSync = teamSyncFunc
_, err := loginsvc.UpsertUser(context.Background(), upsertCmdSkipTeamSync)
require.Nil(t, err)
assert.Nil(t, actualUser)
assert.Nil(t, actualExternalUser)
})
t.Run("login.TeamSync should propagate its errors to the caller", func(t *testing.T) {
teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error {
return errors.New("teamsync test error")
}
loginsvc.TeamSync = teamSyncFunc
_, err := loginsvc.UpsertUser(context.Background(), upsertCmd)
require.Error(t, err)
})
})
}
func TestUpsertUser_crashOnLog_issue62538(t *testing.T) {
authInfoMock := &logintest.AuthInfoServiceFake{}
authInfoMock.ExpectedError = user.ErrUserNotFound
loginsvc := Implementation{
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
}
email := "test_user@example.org"
upsertCmd := &login.UpsertUserCommand{
ExternalUser: &login.ExternalUserInfo{Email: email},
UserLookupParams: login.UserLookupParams{Email: &email},
SignupAllowed: false,
}
var err error
require.NotPanics(t, func() {
_, err = loginsvc.UpsertUser(context.Background(), upsertCmd)
})
require.ErrorIs(t, err, login.ErrSignupNotAllowed)
}
func createSimpleUser() user.User {
user := user.User{
ID: 1,
}
return user
}
func createUserOrgDTO() []*org.UserOrgDTO {
users := []*org.UserOrgDTO{
{
OrgID: 1,
Name: "Bar",
Role: org.RoleViewer,
},
{
OrgID: 10,
Name: "Foo",
Role: org.RoleAdmin,
},
{
OrgID: 11,
Name: "Stuff",
Role: org.RoleViewer,
},
}
return users
}
func createSimpleExternalUser() login.ExternalUserInfo {
externalUser := login.ExternalUserInfo{
AuthModule: login.LDAPAuthModule,
OrgRoles: map[int64]org.RoleType{
1: org.RoleViewer,
},
}
return externalUser
}
func createResponseWithOneErrLastOrgAdminItem() orgtest.OrgListResponse {
remResp := orgtest.OrgListResponse{
{
OrgID: 10,
Response: org.ErrLastOrgAdmin,
},
{
OrgID: 11,
Response: nil,
},
}
return remResp
}

View File

@ -7,16 +7,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
type LoginServiceFake struct{}
func (l *LoginServiceFake) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) (*user.User, error) {
return nil, nil
}
func (l *LoginServiceFake) DisableExternalUser(ctx context.Context, username string) error {
return nil
}
func (l *LoginServiceFake) SetTeamSyncFunc(login.TeamSyncFunc) {}
type AuthInfoServiceFake struct {
login.AuthInfoService
LatestUserID int64
@ -71,13 +61,3 @@ func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, qu
func (a *AuthInfoServiceFake) DeleteUserAuthInfo(ctx context.Context, userID int64) error {
return a.ExpectedError
}
type AuthenticatorFake struct {
ExpectedUser *user.User
ExpectedError error
}
func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error {
query.User = a.ExpectedUser
return a.ExpectedError
}

View File

@ -91,13 +91,6 @@ type RequestURIKey struct{}
// ---------------------
// COMMANDS
type UpsertUserCommand struct {
ReqContext *contextmodel.ReqContext
ExternalUser *ExternalUserInfo
UserLookupParams
SignupAllowed bool
}
type SetAuthInfoCommand struct {
AuthModule string
AuthId string