mirror of https://github.com/grafana/grafana
Add delete user from other services/stores (#51912)
* 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 * Rename DeleteOrgUser to DeleteUserFromAll * Update pkg/services/org/orgimpl/org_test.go Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Update pkg/services/preference/prefimpl/inmemory_test.go Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Rename Acl to ACL * Fix wire after merge with main * Move test to uni test Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>pull/52342/head
parent
5d052be6ff
commit
17ec9cac83
@ -0,0 +1,10 @@ |
||||
package quota |
||||
|
||||
import "errors" |
||||
|
||||
var ErrInvalidQuotaTarget = errors.New("invalid quota target") |
||||
|
||||
type ScopeParameters struct { |
||||
OrgID int64 |
||||
UserID int64 |
||||
} |
||||
@ -0,0 +1,200 @@ |
||||
package quotaimpl |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/quota" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
type Service struct { |
||||
store store |
||||
AuthTokenService models.UserTokenService |
||||
Cfg *setting.Cfg |
||||
SQLStore sqlstore.Store |
||||
Logger log.Logger |
||||
} |
||||
|
||||
func ProvideService(db db.DB, cfg *setting.Cfg, tokenService models.UserTokenService, ss *sqlstore.SQLStore) quota.Service { |
||||
return &Service{ |
||||
store: &sqlStore{db: db}, |
||||
Cfg: cfg, |
||||
AuthTokenService: tokenService, |
||||
SQLStore: ss, |
||||
Logger: log.New("quota_service"), |
||||
} |
||||
} |
||||
|
||||
// QuotaReached checks that quota is reached for a target. Runs CheckQuotaReached and take context and scope parameters from the request context
|
||||
func (s *Service) QuotaReached(c *models.ReqContext, target string) (bool, error) { |
||||
if !s.Cfg.Quota.Enabled { |
||||
return false, nil |
||||
} |
||||
// No request context means this is a background service, like LDAP Background Sync
|
||||
if c == nil { |
||||
return false, nil |
||||
} |
||||
|
||||
var params *quota.ScopeParameters |
||||
if c.IsSignedIn { |
||||
params = "a.ScopeParameters{ |
||||
OrgID: c.OrgId, |
||||
UserID: c.UserId, |
||||
} |
||||
} |
||||
return s.CheckQuotaReached(c.Req.Context(), target, params) |
||||
} |
||||
|
||||
// CheckQuotaReached check that quota is reached for a target. If ScopeParameters are not defined, only global scope is checked
|
||||
func (s *Service) CheckQuotaReached(ctx context.Context, target string, scopeParams *quota.ScopeParameters) (bool, error) { |
||||
if !s.Cfg.Quota.Enabled { |
||||
return false, nil |
||||
} |
||||
// get the list of scopes that this target is valid for. Org, User, Global
|
||||
scopes, err := s.getQuotaScopes(target) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
for _, scope := range scopes { |
||||
s.Logger.Debug("Checking quota", "target", target, "scope", scope) |
||||
|
||||
switch scope.Name { |
||||
case "global": |
||||
if scope.DefaultLimit < 0 { |
||||
continue |
||||
} |
||||
if scope.DefaultLimit == 0 { |
||||
return true, nil |
||||
} |
||||
if target == "session" { |
||||
usedSessions, err := s.AuthTokenService.ActiveTokenCount(ctx) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
if usedSessions > scope.DefaultLimit { |
||||
s.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) |
||||
return true, nil |
||||
} |
||||
continue |
||||
} |
||||
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled()} |
||||
// TODO : move GetGlobalQuotaByTarget to a global quota service
|
||||
if err := s.SQLStore.GetGlobalQuotaByTarget(ctx, &query); err != nil { |
||||
return true, err |
||||
} |
||||
if query.Result.Used >= scope.DefaultLimit { |
||||
return true, nil |
||||
} |
||||
case "org": |
||||
if scopeParams == nil { |
||||
continue |
||||
} |
||||
query := models.GetOrgQuotaByTargetQuery{ |
||||
OrgId: scopeParams.OrgID, |
||||
Target: scope.Target, |
||||
Default: scope.DefaultLimit, |
||||
UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled(), |
||||
} |
||||
// TODO: move GetOrgQuotaByTarget from sqlstore to quota store
|
||||
if err := s.SQLStore.GetOrgQuotaByTarget(ctx, &query); err != nil { |
||||
return true, err |
||||
} |
||||
if query.Result.Limit < 0 { |
||||
continue |
||||
} |
||||
if query.Result.Limit == 0 { |
||||
return true, nil |
||||
} |
||||
|
||||
if query.Result.Used >= query.Result.Limit { |
||||
return true, nil |
||||
} |
||||
case "user": |
||||
if scopeParams == nil || scopeParams.UserID == 0 { |
||||
continue |
||||
} |
||||
query := models.GetUserQuotaByTargetQuery{UserId: scopeParams.UserID, Target: scope.Target, Default: scope.DefaultLimit, UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled()} |
||||
// TODO: move GetUserQuotaByTarget from sqlstore to quota store
|
||||
if err := s.SQLStore.GetUserQuotaByTarget(ctx, &query); err != nil { |
||||
return true, err |
||||
} |
||||
if query.Result.Limit < 0 { |
||||
continue |
||||
} |
||||
if query.Result.Limit == 0 { |
||||
return true, nil |
||||
} |
||||
|
||||
if query.Result.Used >= query.Result.Limit { |
||||
return true, nil |
||||
} |
||||
} |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
func (s *Service) getQuotaScopes(target string) ([]models.QuotaScope, error) { |
||||
scopes := make([]models.QuotaScope, 0) |
||||
switch target { |
||||
case "user": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.User}, |
||||
models.QuotaScope{Name: "org", Target: "org_user", DefaultLimit: s.Cfg.Quota.Org.User}, |
||||
) |
||||
return scopes, nil |
||||
case "org": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.Org}, |
||||
models.QuotaScope{Name: "user", Target: "org_user", DefaultLimit: s.Cfg.Quota.User.Org}, |
||||
) |
||||
return scopes, nil |
||||
case "dashboard": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{ |
||||
Name: "global", |
||||
Target: target, |
||||
DefaultLimit: s.Cfg.Quota.Global.Dashboard, |
||||
}, |
||||
models.QuotaScope{ |
||||
Name: "org", |
||||
Target: target, |
||||
DefaultLimit: s.Cfg.Quota.Org.Dashboard, |
||||
}, |
||||
) |
||||
return scopes, nil |
||||
case "data_source": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.DataSource}, |
||||
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.DataSource}, |
||||
) |
||||
return scopes, nil |
||||
case "api_key": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.ApiKey}, |
||||
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.ApiKey}, |
||||
) |
||||
return scopes, nil |
||||
case "session": |
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.Session}, |
||||
) |
||||
return scopes, nil |
||||
case "alert_rule": // target need to match the respective database name
|
||||
scopes = append(scopes, |
||||
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.AlertRule}, |
||||
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.AlertRule}, |
||||
) |
||||
return scopes, nil |
||||
default: |
||||
return scopes, quota.ErrInvalidQuotaTarget |
||||
} |
||||
} |
||||
|
||||
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error { |
||||
return s.store.DeleteByUser(ctx, userID) |
||||
} |
||||
@ -0,0 +1,28 @@ |
||||
package quotaimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestQuotaService(t *testing.T) { |
||||
quotaStore := &FakeQuotaStore{} |
||||
quotaService := Service{ |
||||
store: quotaStore, |
||||
} |
||||
|
||||
t.Run("delete quota", func(t *testing.T) { |
||||
err := quotaService.DeleteByUser(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
|
||||
type FakeQuotaStore struct { |
||||
ExpectedError error |
||||
} |
||||
|
||||
func (f *FakeQuotaStore) DeleteByUser(ctx context.Context, userID int64) error { |
||||
return f.ExpectedError |
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
package quotaimpl |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db" |
||||
) |
||||
|
||||
type store interface { |
||||
DeleteByUser(context.Context, int64) error |
||||
} |
||||
|
||||
type sqlStore struct { |
||||
db db.DB |
||||
} |
||||
|
||||
func (ss *sqlStore) DeleteByUser(ctx context.Context, userID int64) error { |
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
var rawSQL = "DELETE FROM quota WHERE user_id = ?" |
||||
_, err := sess.Exec(rawSQL, userID) |
||||
return err |
||||
}) |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
package quotaimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestIntegrationQuotaDataAccess(t *testing.T) { |
||||
if testing.Short() { |
||||
t.Skip("skipping integration test") |
||||
} |
||||
|
||||
ss := sqlstore.InitTestDB(t) |
||||
quotaStore := sqlStore{ |
||||
db: ss, |
||||
} |
||||
|
||||
t.Run("quota deleted", func(t *testing.T) { |
||||
err := quotaStore.DeleteByUser(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
@ -0,0 +1,29 @@ |
||||
package quotatest |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/quota" |
||||
) |
||||
|
||||
type FakeQuotaService struct { |
||||
reached bool |
||||
err error |
||||
} |
||||
|
||||
func NewQuotaServiceFake() *FakeQuotaService { |
||||
return &FakeQuotaService{} |
||||
} |
||||
|
||||
func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) { |
||||
return f.reached, f.err |
||||
} |
||||
|
||||
func (f *FakeQuotaService) CheckQuotaReached(c context.Context, target string, params *quota.ScopeParameters) (bool, error) { |
||||
return f.reached, f.err |
||||
} |
||||
|
||||
func (f *FakeQuotaService) DeleteByUser(c context.Context, userID int64) error { |
||||
return f.err |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
package userauth |
||||
|
||||
import "context" |
||||
|
||||
type Service interface { |
||||
Delete(ctx context.Context, userID int64) error |
||||
DeleteToken(ctx context.Context, userID int64) error |
||||
} |
||||
@ -0,0 +1,33 @@ |
||||
package userauthimpl |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db" |
||||
) |
||||
|
||||
type store interface { |
||||
Delete(context.Context, int64) error |
||||
DeleteToken(context.Context, int64) error |
||||
} |
||||
|
||||
type sqlStore struct { |
||||
db db.DB |
||||
} |
||||
|
||||
func (ss *sqlStore) Delete(ctx context.Context, userID int64) error { |
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
var rawSQL = "DELETE FROM user_auth WHERE user_id = ?" |
||||
_, err := sess.Exec(rawSQL, userID) |
||||
return err |
||||
}) |
||||
} |
||||
|
||||
func (ss *sqlStore) DeleteToken(ctx context.Context, userID int64) error { |
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
var rawSQL = "DELETE FROM user_auth_token WHERE user_id = ?" |
||||
_, err := sess.Exec(rawSQL, userID) |
||||
return err |
||||
}) |
||||
} |
||||
@ -0,0 +1,30 @@ |
||||
package userauthimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestIntegrationUserAuthDataAccess(t *testing.T) { |
||||
if testing.Short() { |
||||
t.Skip("skipping integration test") |
||||
} |
||||
|
||||
ss := sqlstore.InitTestDB(t) |
||||
userAuthStore := sqlStore{ |
||||
db: ss, |
||||
} |
||||
|
||||
t.Run("delete user auth", func(t *testing.T) { |
||||
err := userAuthStore.Delete(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
|
||||
t.Run("delete user auth token", func(t *testing.T) { |
||||
err := userAuthStore.DeleteToken(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
@ -0,0 +1,28 @@ |
||||
package userauthimpl |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db" |
||||
"github.com/grafana/grafana/pkg/services/userauth" |
||||
) |
||||
|
||||
type Service struct { |
||||
store store |
||||
} |
||||
|
||||
func ProvideService(db db.DB) userauth.Service { |
||||
return &Service{ |
||||
store: &sqlStore{ |
||||
db: db, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (s *Service) Delete(ctx context.Context, userID int64) error { |
||||
return s.store.Delete(ctx, userID) |
||||
} |
||||
|
||||
func (s *Service) DeleteToken(ctx context.Context, userID int64) error { |
||||
return s.store.DeleteToken(ctx, userID) |
||||
} |
||||
@ -0,0 +1,37 @@ |
||||
package userauthimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestUserAuthService(t *testing.T) { |
||||
userAuthStore := &FakeUserAuthStore{} |
||||
userAuthService := Service{ |
||||
store: userAuthStore, |
||||
} |
||||
|
||||
t.Run("delete user", func(t *testing.T) { |
||||
err := userAuthService.Delete(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
|
||||
t.Run("delete token", func(t *testing.T) { |
||||
err := userAuthService.DeleteToken(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
|
||||
type FakeUserAuthStore struct { |
||||
ExpectedError error |
||||
} |
||||
|
||||
func (f *FakeUserAuthStore) Delete(ctx context.Context, userID int64) error { |
||||
return f.ExpectedError |
||||
} |
||||
|
||||
func (f *FakeUserAuthStore) DeleteToken(ctx context.Context, userID int64) error { |
||||
return f.ExpectedError |
||||
} |
||||
Loading…
Reference in new issue