mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 17:15:08 -05:00
Chore: Split get user by ID (#52442)
* Remove user from preferences, stars, orguser, team member * Fix lint * Add Delete user from org and dashboard acl * Delete user from user auth * Add DeleteUser to quota * Add test files and adjust user auth store * Rename package in wire for user auth * Import Quota Service interface in other services * do the same in tests * fix lint tests * Fix tests * Add some tests * Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser * Rename DeleteUser to DeleteByUser in quota * changing a method name in few additional places * Fix in other places * Fix lint * Fix tests * Chore: Split Delete User method * Add fakes for userauth * Add mock for access control Delete User permossion, use interface * Use interface for ream guardian * Add simple fake for dashboard acl * Add go routines, clean up, use interfaces * fix lint * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Split get user by ID * Use new method in api * Add tests * Aplly emthod in auth info service * Fix lint and some tests * Fix get user by ID * Fix lint Remove unused fakes * Use split get user id in admin users * Use GetbyID in cli commands * Clean up after merge * Remove commented out code * Clena up imports * add back ) * Fix wire generation for runner after merge with main Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
co-authored by
Sofia Papagiannaki
parent
64488f6b90
commit
fab6c38c95
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -18,13 +19,15 @@ type AuthInfoStore struct {
|
||||
sqlStore sqlstore.Store
|
||||
secretsService secrets.Service
|
||||
logger log.Logger
|
||||
userService user.Service
|
||||
}
|
||||
|
||||
func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Service) *AuthInfoStore {
|
||||
func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Service, userService user.Service) login.Store {
|
||||
store := &AuthInfoStore{
|
||||
sqlStore: sqlStore,
|
||||
secretsService: secretsService,
|
||||
logger: log.New("login.authinfo.store"),
|
||||
userService: userService,
|
||||
}
|
||||
InitMetrics()
|
||||
return store
|
||||
@@ -221,12 +224,13 @@ func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAu
|
||||
}
|
||||
|
||||
func (s *AuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) {
|
||||
query := models.GetUserByIdQuery{Id: id}
|
||||
if err := s.sqlStore.GetUserById(ctx, &query); err != nil {
|
||||
query := user.GetUserByIDQuery{ID: id}
|
||||
user, err := s.userService.GetByID(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthInfoStore) GetUserByLogin(ctx context.Context, login string) (*user.User, error) {
|
||||
|
||||
@@ -2,62 +2,38 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type LoginStats struct {
|
||||
DuplicateUserEntries int `xorm:"duplicate_user_entries"`
|
||||
MixedCasedUsers int `xorm:"mixed_cased_users"`
|
||||
}
|
||||
|
||||
const (
|
||||
ExporterName = "grafana"
|
||||
metricsCollectionInterval = time.Second * 60 * 4 // every 4 hours, indication of duplicate users
|
||||
)
|
||||
|
||||
var (
|
||||
// MStatDuplicateUserEntries is a indication metric gauge for number of users with duplicate emails or logins
|
||||
MStatDuplicateUserEntries prometheus.Gauge
|
||||
|
||||
// MStatHasDuplicateEntries is a metric for if there is duplicate users
|
||||
MStatHasDuplicateEntries prometheus.Gauge
|
||||
|
||||
// MStatMixedCasedUsers is a metric for if there is duplicate users
|
||||
MStatMixedCasedUsers prometheus.Gauge
|
||||
|
||||
once sync.Once
|
||||
Initialised bool = false
|
||||
)
|
||||
|
||||
func InitMetrics() {
|
||||
once.Do(func() {
|
||||
MStatDuplicateUserEntries = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
login.Once.Do(func() {
|
||||
login.MStatDuplicateUserEntries = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "stat_users_total_duplicate_user_entries",
|
||||
Help: "total number of duplicate user entries by email or login",
|
||||
Namespace: ExporterName,
|
||||
Namespace: login.ExporterName,
|
||||
})
|
||||
|
||||
MStatHasDuplicateEntries = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
login.MStatHasDuplicateEntries = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "stat_users_has_duplicate_user_entries",
|
||||
Help: "instance has duplicate user entries by email or login",
|
||||
Namespace: ExporterName,
|
||||
Namespace: login.ExporterName,
|
||||
})
|
||||
|
||||
MStatMixedCasedUsers = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
login.MStatMixedCasedUsers = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "stat_users_total_mixed_cased_users",
|
||||
Help: "total number of users with upper and lower case logins or emails",
|
||||
Namespace: ExporterName,
|
||||
Namespace: login.ExporterName,
|
||||
})
|
||||
|
||||
prometheus.MustRegister(
|
||||
MStatDuplicateUserEntries,
|
||||
MStatHasDuplicateEntries,
|
||||
MStatMixedCasedUsers,
|
||||
login.MStatDuplicateUserEntries,
|
||||
login.MStatHasDuplicateEntries,
|
||||
login.MStatMixedCasedUsers,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -66,7 +42,7 @@ func (s *AuthInfoStore) RunMetricsCollection(ctx context.Context) error {
|
||||
if _, err := s.GetLoginStats(ctx); err != nil {
|
||||
s.logger.Warn("Failed to get authinfo metrics", "error", err.Error())
|
||||
}
|
||||
updateStatsTicker := time.NewTicker(metricsCollectionInterval)
|
||||
updateStatsTicker := time.NewTicker(login.MetricsCollectionInterval)
|
||||
defer updateStatsTicker.Stop()
|
||||
|
||||
for {
|
||||
@@ -81,8 +57,8 @@ func (s *AuthInfoStore) RunMetricsCollection(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (LoginStats, error) {
|
||||
var stats LoginStats
|
||||
func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (login.LoginStats, error) {
|
||||
var stats login.LoginStats
|
||||
outerErr := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
rawSQL := `SELECT
|
||||
(SELECT COUNT(*) FROM (` + s.duplicateUserEntriesSQL(ctx) + `) AS d WHERE (d.dup_login IS NOT NULL OR d.dup_email IS NOT NULL)) as duplicate_user_entries,
|
||||
@@ -96,14 +72,14 @@ func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (LoginStats, error) {
|
||||
}
|
||||
|
||||
// set prometheus metrics stats
|
||||
MStatDuplicateUserEntries.Set(float64(stats.DuplicateUserEntries))
|
||||
login.MStatDuplicateUserEntries.Set(float64(stats.DuplicateUserEntries))
|
||||
if stats.DuplicateUserEntries == 0 {
|
||||
MStatHasDuplicateEntries.Set(float64(0))
|
||||
login.MStatHasDuplicateEntries.Set(float64(0))
|
||||
} else {
|
||||
MStatHasDuplicateEntries.Set(float64(1))
|
||||
login.MStatHasDuplicateEntries.Set(float64(1))
|
||||
}
|
||||
|
||||
MStatMixedCasedUsers.Set(float64(stats.MixedCasedUsers))
|
||||
login.MStatMixedCasedUsers.Set(float64(stats.MixedCasedUsers))
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -115,14 +91,12 @@ func (s *AuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]inter
|
||||
s.logger.Error("Failed to get login stats", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m["stats.users.duplicate_user_entries"] = loginStats.DuplicateUserEntries
|
||||
if loginStats.DuplicateUserEntries > 0 {
|
||||
m["stats.users.has_duplicate_user_entries"] = 1
|
||||
} else {
|
||||
m["stats.users.has_duplicate_user_entries"] = 0
|
||||
}
|
||||
|
||||
m["stats.users.mixed_cased_users"] = loginStats.MixedCasedUsers
|
||||
|
||||
return m, nil
|
||||
|
||||
@@ -2,26 +2,26 @@ package authinfoservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||
secretstore "github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
//nolint:goconst
|
||||
func TestUserAuth(t *testing.T) {
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
secretsService := secretsManager.SetupTestService(t, secretstore.ProvideSecretsStore(sqlStore))
|
||||
authInfoStore := database.ProvideAuthInfoStore(sqlStore, secretsService)
|
||||
authInfoStore := newFakeAuthInfoStore()
|
||||
srv := ProvideAuthInfoService(
|
||||
&OSSUserProtectionImpl{},
|
||||
authInfoStore,
|
||||
@@ -42,7 +42,11 @@ func TestUserAuth(t *testing.T) {
|
||||
t.Run("Can find existing user", func(t *testing.T) {
|
||||
// By Login
|
||||
login := "loginuser0"
|
||||
|
||||
authInfoStore.ExpectedUser = &user.User{
|
||||
Login: "loginuser0",
|
||||
ID: 1,
|
||||
Email: "user1@test.com",
|
||||
}
|
||||
query := &models.GetUserByAuthInfoQuery{UserLookupParams: models.UserLookupParams{Login: &login}}
|
||||
usr, err := srv.LookupAndUpdate(context.Background(), query)
|
||||
|
||||
@@ -69,6 +73,7 @@ func TestUserAuth(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, usr.Email, email)
|
||||
|
||||
authInfoStore.ExpectedUser = nil
|
||||
// Don't find nonexistent user
|
||||
email = "nonexistent@test.com"
|
||||
|
||||
@@ -82,6 +87,8 @@ func TestUserAuth(t *testing.T) {
|
||||
|
||||
t.Run("Can set & locate by AuthModule and AuthId", func(t *testing.T) {
|
||||
// get nonexistent user_auth entry
|
||||
authInfoStore.ExpectedUser = &user.User{}
|
||||
authInfoStore.ExpectedError = user.ErrUserNotFound
|
||||
query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
usr, err := srv.LookupAndUpdate(context.Background(), query)
|
||||
|
||||
@@ -90,7 +97,9 @@ func TestUserAuth(t *testing.T) {
|
||||
|
||||
// create user_auth entry
|
||||
login := "loginuser0"
|
||||
|
||||
authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""}
|
||||
authInfoStore.ExpectedError = nil
|
||||
authInfoStore.ExpectedOAuth = &models.UserAuth{Id: 1}
|
||||
query.UserLookupParams.Login = &login
|
||||
usr, err = srv.LookupAndUpdate(context.Background(), query)
|
||||
|
||||
@@ -107,6 +116,7 @@ func TestUserAuth(t *testing.T) {
|
||||
// get with non-matching id
|
||||
idPlusOne := usr.ID + 1
|
||||
|
||||
authInfoStore.ExpectedUser.Login = "loginuser1"
|
||||
query.UserLookupParams.UserID = &idPlusOne
|
||||
usr, err = srv.LookupAndUpdate(context.Background(), query)
|
||||
|
||||
@@ -127,6 +137,8 @@ func TestUserAuth(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authInfoStore.ExpectedUser = nil
|
||||
authInfoStore.ExpectedError = user.ErrUserNotFound
|
||||
// get via user_auth for deleted user
|
||||
query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
usr, err = srv.LookupAndUpdate(context.Background(), query)
|
||||
@@ -147,7 +159,16 @@ func TestUserAuth(t *testing.T) {
|
||||
|
||||
// Find a user to set tokens on
|
||||
login := "loginuser0"
|
||||
|
||||
authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""}
|
||||
authInfoStore.ExpectedError = nil
|
||||
authInfoStore.ExpectedOAuth = &models.UserAuth{
|
||||
Id: 1,
|
||||
OAuthAccessToken: token.AccessToken,
|
||||
OAuthRefreshToken: token.RefreshToken,
|
||||
OAuthTokenType: token.TokenType,
|
||||
OAuthIdToken: idToken,
|
||||
OAuthExpiry: token.Expiry,
|
||||
}
|
||||
// Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table
|
||||
query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{
|
||||
Login: &login,
|
||||
@@ -220,7 +241,7 @@ func TestUserAuth(t *testing.T) {
|
||||
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, user.Login, login)
|
||||
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test2"
|
||||
// Get the latest entry by not supply an authmodule or authid
|
||||
getAuthQuery := &models.GetAuthInfoQuery{
|
||||
UserId: user.ID,
|
||||
@@ -236,7 +257,7 @@ func TestUserAuth(t *testing.T) {
|
||||
err = authInfoStore.UpdateAuthInfo(context.Background(), updateAuthCmd)
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test1"
|
||||
// Get the latest entry by not supply an authmodule or authid
|
||||
getAuthQuery = &models.GetAuthInfoQuery{
|
||||
UserId: user.ID,
|
||||
@@ -292,6 +313,7 @@ func TestUserAuth(t *testing.T) {
|
||||
getAuthQuery := &models.GetAuthInfoQuery{
|
||||
UserId: user.ID,
|
||||
}
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test2"
|
||||
|
||||
err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery)
|
||||
|
||||
@@ -314,7 +336,8 @@ func TestUserAuth(t *testing.T) {
|
||||
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, user.Login, login)
|
||||
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test1"
|
||||
authInfoStore.ExpectedOAuth.OAuthAccessToken = "access_token"
|
||||
err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery)
|
||||
|
||||
require.Nil(t, err)
|
||||
@@ -327,6 +350,7 @@ func TestUserAuth(t *testing.T) {
|
||||
user, err = srv.LookupAndUpdate(context.Background(), queryTwo)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, user.Login, login)
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test2"
|
||||
|
||||
err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery)
|
||||
require.Nil(t, err)
|
||||
@@ -337,10 +361,11 @@ func TestUserAuth(t *testing.T) {
|
||||
UserId: user.ID,
|
||||
AuthModule: "test1",
|
||||
}
|
||||
authInfoStore.ExpectedOAuth.AuthModule = "test1"
|
||||
|
||||
err = authInfoStore.GetAuthInfo(context.Background(), getAuthQueryUnchanged)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "test1", getAuthQueryUnchanged.Result.AuthModule)
|
||||
require.Less(t, getAuthQueryUnchanged.Result.Created, getAuthQuery.Result.Created)
|
||||
})
|
||||
|
||||
t.Run("Can set & locate by generic oauth auth module and user id", func(t *testing.T) {
|
||||
@@ -364,11 +389,14 @@ func TestUserAuth(t *testing.T) {
|
||||
query = &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{
|
||||
Login: &otherLoginUser,
|
||||
}}
|
||||
authInfoStore.ExpectedError = errors.New("some error")
|
||||
|
||||
user, err = srv.LookupAndUpdate(context.Background(), query)
|
||||
database.GetTime = time.Now
|
||||
|
||||
require.NotNil(t, err)
|
||||
require.Nil(t, user)
|
||||
authInfoStore.ExpectedError = nil
|
||||
})
|
||||
|
||||
t.Run("should be able to run loginstats query in all dbs", func(t *testing.T) {
|
||||
@@ -426,8 +454,18 @@ func TestUserAuth(t *testing.T) {
|
||||
}
|
||||
_, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// require stats to populate
|
||||
authInfoStore.ExpectedUser = &user.User{
|
||||
Email: "userduplicatetest1@test.com",
|
||||
Name: "user name 1",
|
||||
Login: "user_duplicate_test_1_login",
|
||||
}
|
||||
authInfoStore.ExpectedDuplicateUserEntries = 2
|
||||
authInfoStore.ExpectedHasDuplicateUserEntries = 1
|
||||
authInfoStore.ExpectedLoginStats = login.LoginStats{
|
||||
DuplicateUserEntries: 2,
|
||||
MixedCasedUsers: 1,
|
||||
}
|
||||
// require metrics and statistics to be 2
|
||||
m, err := srv.authInfoStore.CollectLoginStats(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, m["stats.users.duplicate_user_entries"])
|
||||
@@ -438,3 +476,67 @@ func TestUserAuth(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type FakeAuthInfoStore struct {
|
||||
ExpectedError error
|
||||
ExpectedUser *user.User
|
||||
ExpectedOAuth *models.UserAuth
|
||||
ExpectedDuplicateUserEntries int
|
||||
ExpectedHasDuplicateUserEntries int
|
||||
ExpectedLoginStats login.LoginStats
|
||||
}
|
||||
|
||||
func newFakeAuthInfoStore() *FakeAuthInfoStore {
|
||||
return &FakeAuthInfoStore{}
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error {
|
||||
query.Result = f.ExpectedOAuth
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
func (f *FakeAuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) GetUserByLogin(ctx context.Context, login string) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) GetUserByEmail(ctx context.Context, email string) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
var res = make(map[string]interface{})
|
||||
res["stats.users.duplicate_user_entries"] = f.ExpectedDuplicateUserEntries
|
||||
res["stats.users.has_duplicate_user_entries"] = f.ExpectedHasDuplicateUserEntries
|
||||
res["stats.users.duplicate_user_entries_by_login"] = 0
|
||||
res["stats.users.has_duplicate_user_entries_by_login"] = 0
|
||||
res["stats.users.duplicate_user_entries_by_email"] = 0
|
||||
res["stats.users.has_duplicate_user_entries_by_email"] = 0
|
||||
res["stats.users.mixed_cased_users"] = f.ExpectedLoginStats.MixedCasedUsers
|
||||
return res, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) RunMetricsCollection(ctx context.Context) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeAuthInfoStore) GetLoginStats(ctx context.Context) (login.LoginStats, error) {
|
||||
return f.ExpectedLoginStats, f.ExpectedError
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type LoginStats struct {
|
||||
DuplicateUserEntries int `xorm:"duplicate_user_entries"`
|
||||
MixedCasedUsers int `xorm:"mixed_cased_users"`
|
||||
}
|
||||
|
||||
const (
|
||||
ExporterName = "grafana"
|
||||
MetricsCollectionInterval = time.Second * 60 * 4 // every 4 hours, indication of duplicate users
|
||||
)
|
||||
|
||||
var (
|
||||
// MStatDuplicateUserEntries is a indication metric gauge for number of users with duplicate emails or logins
|
||||
MStatDuplicateUserEntries prometheus.Gauge
|
||||
|
||||
// MStatHasDuplicateEntries is a metric for if there is duplicate users
|
||||
MStatHasDuplicateEntries prometheus.Gauge
|
||||
|
||||
// MStatMixedCasedUsers is a metric for if there is duplicate users
|
||||
MStatMixedCasedUsers prometheus.Gauge
|
||||
|
||||
Once sync.Once
|
||||
Initialised bool = false
|
||||
)
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
@@ -24,5 +23,5 @@ type Store interface {
|
||||
GetUserByEmail(ctx context.Context, email string) (*user.User, error)
|
||||
CollectLoginStats(ctx context.Context) (map[string]interface{}, error)
|
||||
RunMetricsCollection(ctx context.Context) error
|
||||
GetLoginStats(ctx context.Context) (database.LoginStats, error)
|
||||
GetLoginStats(ctx context.Context) (LoginStats, error)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -69,3 +71,28 @@ func (u *User) NameOrFallback() string {
|
||||
type DeleteUserCommand struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
type GetUserByIDQuery struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
type ErrCaseInsensitiveLoginConflict struct {
|
||||
Users []User
|
||||
}
|
||||
|
||||
func (e *ErrCaseInsensitiveLoginConflict) Unwrap() error {
|
||||
return ErrCaseInsensitive
|
||||
}
|
||||
|
||||
func (e *ErrCaseInsensitiveLoginConflict) Error() string {
|
||||
n := len(e.Users)
|
||||
|
||||
userStrings := make([]string, 0, n)
|
||||
for _, v := range e.Users {
|
||||
userStrings = append(userStrings, fmt.Sprintf("%s (email:%s, id:%d)", v.Login, v.Email, v.ID))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"Found a conflict in user login information. %d users already exist with either the same login or email: [%s].",
|
||||
n, strings.Join(userStrings, ", "))
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ import (
|
||||
type Service interface {
|
||||
Create(context.Context, *CreateUserCommand) (*User, error)
|
||||
Delete(context.Context, *DeleteUserCommand) error
|
||||
GetByID(context.Context, *GetUserByIDQuery) (*User, error)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
type store interface {
|
||||
Insert(context.Context, *user.User) (int64, error)
|
||||
Get(context.Context, *user.User) (*user.User, error)
|
||||
GetByID(context.Context, int64) (*user.User, error)
|
||||
GetNotServiceAccount(context.Context, int64) (*user.User, error)
|
||||
Delete(context.Context, int64) error
|
||||
CaseInsensitiveLoginConflict(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type sqlStore struct {
|
||||
@@ -91,8 +93,42 @@ func (ss *sqlStore) GetNotServiceAccount(ctx context.Context, userID int64) (*us
|
||||
return &usr, err
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetByID(ctx context.Context, userID int64) (*user.User, error) {
|
||||
var usr user.User
|
||||
|
||||
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.ID(&userID).
|
||||
Where(ss.notServiceAccountFilter()).
|
||||
Get(&usr)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return user.ErrUserNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &usr, err
|
||||
}
|
||||
|
||||
func (ss *sqlStore) notServiceAccountFilter() string {
|
||||
return fmt.Sprintf("%s.is_service_account = %s",
|
||||
ss.dialect.Quote("user"),
|
||||
ss.dialect.BooleanStr(false))
|
||||
}
|
||||
|
||||
func (ss *sqlStore) CaseInsensitiveLoginConflict(ctx context.Context, login, email string) error {
|
||||
users := make([]user.User, 0)
|
||||
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)",
|
||||
email, login).Find(&users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(users) > 1 {
|
||||
return &user.ErrCaseInsensitiveLoginConflict{Users: users}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package userimpl
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -32,6 +31,8 @@ type Service struct {
|
||||
userAuthService userauth.Service
|
||||
quotaService quota.Service
|
||||
accessControlStore accesscontrol.AccessControl
|
||||
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func ProvideService(
|
||||
@@ -44,6 +45,7 @@ func ProvideService(
|
||||
userAuthService userauth.Service,
|
||||
quotaService quota.Service,
|
||||
accessControlStore accesscontrol.AccessControl,
|
||||
cfg *setting.Cfg,
|
||||
) user.Service {
|
||||
return &Service{
|
||||
store: &sqlStore{
|
||||
@@ -58,6 +60,7 @@ func ProvideService(
|
||||
userAuthService: userAuthService,
|
||||
quotaService: quotaService,
|
||||
accessControlStore: accessControlStore,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use
|
||||
func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error {
|
||||
_, err := s.store.GetNotServiceAccount(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user with not service account: %w", err)
|
||||
return err
|
||||
}
|
||||
// delete from all the stores
|
||||
if err := s.store.Delete(ctx, cmd.UserID); err != nil {
|
||||
@@ -225,3 +228,16 @@ func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) {
|
||||
user, err := s.store.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.cfg.CaseInsensitiveLogin {
|
||||
if err := s.store.CaseInsensitiveLoginConflict(ctx, user.Login, user.Email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/teamguardian/manager"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/userauth/userauthtest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -40,7 +42,67 @@ func TestUserService(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("create user", func(t *testing.T) {
|
||||
_, err := userService.Create(context.Background(), &user.CreateUserCommand{})
|
||||
_, err := userService.Create(context.Background(), &user.CreateUserCommand{
|
||||
Email: "email",
|
||||
Login: "login",
|
||||
Name: "name",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("get user by ID", func(t *testing.T) {
|
||||
userService.cfg = setting.NewCfg()
|
||||
userService.cfg.CaseInsensitiveLogin = false
|
||||
userStore.ExpectedUser = &user.User{ID: 1, Email: "email", Login: "login", Name: "name"}
|
||||
u, err := userService.GetByID(context.Background(), &user.GetUserByIDQuery{ID: 1})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "login", u.Login)
|
||||
require.Equal(t, "name", u.Name)
|
||||
require.Equal(t, "email", u.Email)
|
||||
})
|
||||
|
||||
t.Run("get user by ID with case insensitive login", func(t *testing.T) {
|
||||
userService.cfg = setting.NewCfg()
|
||||
userService.cfg.CaseInsensitiveLogin = true
|
||||
userStore.ExpectedUser = &user.User{ID: 1, Email: "email", Login: "login", Name: "name"}
|
||||
u, err := userService.GetByID(context.Background(), &user.GetUserByIDQuery{ID: 1})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "login", u.Login)
|
||||
require.Equal(t, "name", u.Name)
|
||||
require.Equal(t, "email", u.Email)
|
||||
})
|
||||
|
||||
t.Run("delete user store returns error", func(t *testing.T) {
|
||||
userStore.ExpectedDeleteUserError = user.ErrUserNotFound
|
||||
t.Cleanup(func() {
|
||||
userStore.ExpectedDeleteUserError = nil
|
||||
})
|
||||
err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1})
|
||||
require.Error(t, err, user.ErrUserNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete user returns from team", func(t *testing.T) {
|
||||
teamMemberService.ExpectedError = errors.New("some error")
|
||||
t.Cleanup(func() {
|
||||
teamMemberService.ExpectedError = nil
|
||||
})
|
||||
err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("delete user returns from team and pref", func(t *testing.T) {
|
||||
teamMemberService.ExpectedError = errors.New("some error")
|
||||
preferenceService.ExpectedError = errors.New("some error 2")
|
||||
t.Cleanup(func() {
|
||||
teamMemberService.ExpectedError = nil
|
||||
preferenceService.ExpectedError = nil
|
||||
})
|
||||
err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("delete user successfully", func(t *testing.T) {
|
||||
err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -104,3 +166,11 @@ func (f *FakeUserStore) Delete(ctx context.Context, userID int64) error {
|
||||
func (f *FakeUserStore) GetNotServiceAccount(ctx context.Context, userID int64) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeUserStore) GetByID(context.Context, int64) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeUserStore) CaseInsensitiveLoginConflict(context.Context, string, string) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
||||
@@ -22,3 +22,7 @@ func (f *FakeUserService) Create(ctx context.Context, cmd *user.CreateUserComman
|
||||
func (f *FakeUserService) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakeUserService) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) {
|
||||
return f.ExpectedUser, f.ExpectedError
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user