mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: Cache basic roles and teams permissions (#87043)
* RBAC: Cache basic roles permissions * Cache teams permissions * Set cache TTL to 1 minute * Add OSS implementation * Fetch basic role permissions correctly * fix conflict_user_command * Fix teams permissions query * Add traces for GetUserPermissions * Fix folders tests * Fix colflict user command * Update store mock * Fix linter error * Reuse GetUserPermissions for fetching basic roles * tests for GetTeamsPermissions * pre-allocate slice capacity * Fix linter
This commit is contained in:
@@ -448,7 +448,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
||||
|
||||
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), features)
|
||||
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), features, tracing.InitializeTracerForTest())
|
||||
|
||||
quotaSrv := quotatest.New(false, nil)
|
||||
|
||||
|
||||
@@ -83,7 +83,13 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx
|
||||
return nil, fmt.Errorf("%v: %w", "failed to get user service", err)
|
||||
}
|
||||
routing := routing.ProvideRegister()
|
||||
acService, err := acimpl.ProvideService(cfg, s, routing, nil, nil, features)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", "failed to initialize tracer config", err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err)
|
||||
}
|
||||
acService, err := acimpl.ProvideService(cfg, s, routing, nil, nil, features, tracer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ type Service interface {
|
||||
//go:generate mockery --name Store --structname MockStore --outpkg actest --filename store_mock.go --output ./actest/
|
||||
type Store interface {
|
||||
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error)
|
||||
GetBasicRolesPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error)
|
||||
GetTeamsPermissions(ctx context.Context, query GetUserPermissionsQuery) (map[int64][]Permission, error)
|
||||
SearchUsersPermissions(ctx context.Context, orgID int64, options SearchOptions) (map[int64][]Permission, error)
|
||||
GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error)
|
||||
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -33,7 +35,7 @@ import (
|
||||
var _ plugins.RoleRegistry = &Service{}
|
||||
|
||||
const (
|
||||
cacheTTL = 10 * time.Second
|
||||
cacheTTL = 60 * time.Second
|
||||
)
|
||||
|
||||
var SharedWithMeFolderPermission = accesscontrol.Permission{
|
||||
@@ -44,8 +46,8 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{
|
||||
var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
||||
accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles) (*Service, error) {
|
||||
service := ProvideOSSService(cfg, database.ProvideService(db), cache, features)
|
||||
accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (*Service, error) {
|
||||
service := ProvideOSSService(cfg, database.ProvideService(db), cache, features, tracer)
|
||||
|
||||
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
||||
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
||||
@@ -63,7 +65,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service {
|
||||
func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store, cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service {
|
||||
s := &Service{
|
||||
cache: cache,
|
||||
cfg: cfg,
|
||||
@@ -71,6 +73,7 @@ func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store, cache *local
|
||||
log: log.New("accesscontrol.service"),
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
store: store,
|
||||
tracer: tracer,
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -85,6 +88,7 @@ type Service struct {
|
||||
registrations accesscontrol.RegistrationList
|
||||
roles map[string]*accesscontrol.RoleDTO
|
||||
store accesscontrol.Store
|
||||
tracer tracing.Tracer
|
||||
}
|
||||
|
||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||
@@ -95,6 +99,8 @@ func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||
|
||||
// GetUserPermissions returns user permissions based on built-in roles
|
||||
func (s *Service) GetUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.GetUserPermissionsOSS")
|
||||
defer span.End()
|
||||
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
@@ -136,26 +142,191 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste
|
||||
return append(permissions, dbPermissions...), nil
|
||||
}
|
||||
|
||||
func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
key := permissionCacheKey(user)
|
||||
if !options.ReloadCache {
|
||||
permissions, ok := s.cache.Get(key)
|
||||
if ok {
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
|
||||
s.log.Debug("Using cached permissions", "key", key)
|
||||
return permissions.([]accesscontrol.Permission), nil
|
||||
func (s *Service) getBasicRolePermissions(ctx context.Context, role string, orgID int64) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getBasicRolePermissions")
|
||||
defer span.End()
|
||||
|
||||
permissions := make([]accesscontrol.Permission, 0)
|
||||
if basicRole, ok := s.roles[role]; ok {
|
||||
permissions = append(permissions, basicRole.Permissions...)
|
||||
}
|
||||
|
||||
// Fetch managed role permissions assigned to basic roles
|
||||
dbPermissions, err := s.store.GetBasicRolesPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
Roles: []string{role},
|
||||
OrgID: orgID,
|
||||
RolePrefixes: OSSRolesPrefixes,
|
||||
})
|
||||
permissions = append(permissions, dbPermissions...)
|
||||
return permissions, err
|
||||
}
|
||||
|
||||
func (s *Service) getTeamsPermissions(ctx context.Context, teamIDs []int64, orgID int64) (map[int64][]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getTeamsPermissions")
|
||||
defer span.End()
|
||||
|
||||
teamPermissions, err := s.store.GetTeamsPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
TeamIDs: teamIDs,
|
||||
OrgID: orgID,
|
||||
RolePrefixes: OSSRolesPrefixes,
|
||||
})
|
||||
return teamPermissions, err
|
||||
}
|
||||
|
||||
// Returns only permissions directly assigned to user, without basic role and team permissions
|
||||
func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Requester) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getUserDirectPermissions")
|
||||
defer span.End()
|
||||
|
||||
namespace, identifier := user.GetNamespacedID()
|
||||
|
||||
var userID int64
|
||||
if namespace == authn.NamespaceUser || namespace == authn.NamespaceServiceAccount {
|
||||
var err error
|
||||
userID, err = strconv.ParseInt(identifier, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
|
||||
s.log.Debug("Fetch permissions from store", "key", key)
|
||||
permissions, err := s.getUserPermissions(ctx, user, options)
|
||||
permissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
OrgID: user.GetOrgID(),
|
||||
UserID: userID,
|
||||
RolePrefixes: OSSRolesPrefixes,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
|
||||
permissions = append(permissions, SharedWithMeFolderPermission)
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
basicRolesPermissions, err := s.getCachedBasicRolesPermissions(ctx, user, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamsPermissions, err := s.getCachedTeamsPermissions(ctx, user, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userPermissions, err := s.getCachedUserDirectPermissions(ctx, user, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissions := make([]accesscontrol.Permission, 0, len(basicRolesPermissions)+len(teamsPermissions)+len(userPermissions))
|
||||
permissions = append(permissions, basicRolesPermissions...)
|
||||
permissions = append(permissions, teamsPermissions...)
|
||||
permissions = append(permissions, userPermissions...)
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) getCachedBasicRolesPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getCachedBasicRolesPermissions")
|
||||
defer span.End()
|
||||
|
||||
basicRoles := accesscontrol.GetOrgRoles(user)
|
||||
basicRolesPermissions := make([]accesscontrol.Permission, 0)
|
||||
for _, role := range basicRoles {
|
||||
permissions, err := s.getCachedBasicRolePermissions(ctx, role, user.GetOrgID(), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
basicRolesPermissions = append(basicRolesPermissions, permissions...)
|
||||
}
|
||||
return basicRolesPermissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) getCachedBasicRolePermissions(ctx context.Context, role string, orgID int64, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
key := basicRoleCacheKey(role, orgID)
|
||||
getPermissionsFn := func() ([]accesscontrol.Permission, error) {
|
||||
return s.getBasicRolePermissions(ctx, role, orgID)
|
||||
}
|
||||
return s.getCachedPermissions(ctx, key, getPermissionsFn, options)
|
||||
}
|
||||
|
||||
func (s *Service) getCachedUserDirectPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getCachedUserDirectPermissions")
|
||||
defer span.End()
|
||||
|
||||
key := permissionCacheKey(user)
|
||||
getUserPermissionsFn := func() ([]accesscontrol.Permission, error) {
|
||||
return s.getUserDirectPermissions(ctx, user)
|
||||
}
|
||||
return s.getCachedPermissions(ctx, key, getUserPermissionsFn, options)
|
||||
}
|
||||
|
||||
type GetPermissionsFn = func() ([]accesscontrol.Permission, error)
|
||||
|
||||
// Generic method for getting various permissions from cache
|
||||
func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermissionsFn GetPermissionsFn, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
_, span := s.tracer.Start(ctx, "authz.getCachedTeamsPermissions")
|
||||
defer span.End()
|
||||
|
||||
if !options.ReloadCache {
|
||||
permissions, ok := s.cache.Get(key)
|
||||
if ok {
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
|
||||
return permissions.([]accesscontrol.Permission), nil
|
||||
}
|
||||
}
|
||||
|
||||
span.AddEvent("cache miss")
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
|
||||
permissions, err := getPermissionsFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.log.Debug("Cache permissions", "key", key)
|
||||
s.cache.Set(key, permissions, cacheTTL)
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz.getCachedTeamsPermissions")
|
||||
defer span.End()
|
||||
|
||||
teams := user.GetTeams()
|
||||
orgID := user.GetOrgID()
|
||||
permissions := make([]accesscontrol.Permission, 0)
|
||||
miss := teams
|
||||
|
||||
if !options.ReloadCache {
|
||||
miss = make([]int64, 0)
|
||||
for _, teamID := range teams {
|
||||
key := teamCacheKey(teamID, orgID)
|
||||
teamPermissions, ok := s.cache.Get(key)
|
||||
if ok {
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
|
||||
permissions = append(permissions, teamPermissions.([]accesscontrol.Permission)...)
|
||||
} else {
|
||||
miss = append(miss, teamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(miss) > 0 {
|
||||
span.AddEvent("cache miss")
|
||||
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
|
||||
teamsPermissions, err := s.getTeamsPermissions(ctx, miss, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for teamID, teamPermissions := range teamsPermissions {
|
||||
key := teamCacheKey(teamID, orgID)
|
||||
s.cache.Set(key, teamPermissions, cacheTTL)
|
||||
permissions = append(permissions, teamPermissions...)
|
||||
}
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
@@ -207,10 +378,6 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func permissionCacheKey(user identity.Requester) string {
|
||||
return fmt.Sprintf("rbac-permissions-%s", user.GetCacheKey())
|
||||
}
|
||||
|
||||
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments
|
||||
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error {
|
||||
@@ -484,3 +651,17 @@ func (s *Service) GetRoleByName(ctx context.Context, orgID int64, roleName strin
|
||||
})
|
||||
return role, err
|
||||
}
|
||||
|
||||
func permissionCacheKey(user identity.Requester) string {
|
||||
return fmt.Sprintf("rbac-permissions-%s", user.GetCacheKey())
|
||||
}
|
||||
|
||||
func basicRoleCacheKey(role string, orgID int64) string {
|
||||
roleKey := strings.Replace(role, " ", "_", -1)
|
||||
roleKey = strings.ToLower(roleKey)
|
||||
return fmt.Sprintf("rbac-permissions-basic-role-%d-%s", orgID, roleKey)
|
||||
}
|
||||
|
||||
func teamCacheKey(teamID int64, orgID int64) string {
|
||||
return fmt.Sprintf("rbac-permissions-team-%d-%d", orgID, teamID)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -66,6 +67,7 @@ func TestUsageMetrics(t *testing.T) {
|
||||
database.ProvideService(db.InitTestDB(t)),
|
||||
localcache.ProvideService(),
|
||||
featuremgmt.WithFeatures(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
)
|
||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||
})
|
||||
|
||||
@@ -76,16 +76,26 @@ func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolve
|
||||
}
|
||||
|
||||
type FakeStore struct {
|
||||
ExpectedUserPermissions []accesscontrol.Permission
|
||||
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
|
||||
ExpectedUsersRoles map[int64][]string
|
||||
ExpectedErr error
|
||||
ExpectedUserPermissions []accesscontrol.Permission
|
||||
ExpectedBasicRolesPermissions []accesscontrol.Permission
|
||||
ExpectedTeamsPermissions map[int64][]accesscontrol.Permission
|
||||
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
|
||||
ExpectedUsersRoles map[int64][]string
|
||||
ExpectedErr error
|
||||
}
|
||||
|
||||
func (f FakeStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) {
|
||||
return f.ExpectedUserPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeStore) GetBasicRolesPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) {
|
||||
return f.ExpectedBasicRolesPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeStore) GetTeamsPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) (map[int64][]accesscontrol.Permission, error) {
|
||||
return f.ExpectedTeamsPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
||||
return f.ExpectedUsersPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.42.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.43.0. DO NOT EDIT.
|
||||
|
||||
package actest
|
||||
|
||||
@@ -33,6 +33,24 @@ func (_m *MockStore) DeleteExternalServiceRole(ctx context.Context, externalServ
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteTeamPermissions provides a mock function with given fields: ctx, orgID, teamID
|
||||
func (_m *MockStore) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error {
|
||||
ret := _m.Called(ctx, orgID, teamID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteTeamPermissions")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
|
||||
r0 = rf(ctx, orgID, teamID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteUserPermissions provides a mock function with given fields: ctx, orgID, userID
|
||||
func (_m *MockStore) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
|
||||
ret := _m.Called(ctx, orgID, userID)
|
||||
@@ -51,22 +69,64 @@ func (_m *MockStore) DeleteUserPermissions(ctx context.Context, orgID int64, use
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteTeamPermissions provides a mock function with given fields: ctx, orgID, teamID
|
||||
func (_m *MockStore) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error {
|
||||
ret := _m.Called(ctx, orgID, teamID)
|
||||
// GetBasicRolesPermissions provides a mock function with given fields: ctx, query
|
||||
func (_m *MockStore) GetBasicRolesPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteTeamPermissions")
|
||||
panic("no return value specified for GetBasicRolesPermissions")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
|
||||
r0 = rf(ctx, orgID, teamID)
|
||||
var r0 []accesscontrol.Permission
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error)); ok {
|
||||
return rf(ctx, query)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.GetUserPermissionsQuery) []accesscontrol.Permission); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]accesscontrol.Permission)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
if rf, ok := ret.Get(1).(func(context.Context, accesscontrol.GetUserPermissionsQuery) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTeamsPermissions provides a mock function with given fields: ctx, query
|
||||
func (_m *MockStore) GetTeamsPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) (map[int64][]accesscontrol.Permission, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetTeamsPermissions")
|
||||
}
|
||||
|
||||
var r0 map[int64][]accesscontrol.Permission
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.GetUserPermissionsQuery) (map[int64][]accesscontrol.Permission, error)); ok {
|
||||
return rf(ctx, query)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.GetUserPermissionsQuery) map[int64][]accesscontrol.Permission); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[int64][]accesscontrol.Permission)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, accesscontrol.GetUserPermissionsQuery) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserPermissions provides a mock function with given fields: ctx, query
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
// teamAssignsSQL is a query to select all users' team assignments.
|
||||
teamAssignsSQL = `SELECT tm.user_id, tr.org_id, tr.role_id
|
||||
FROM team_role AS tr
|
||||
FROM team_role AS tr
|
||||
INNER JOIN team_member AS tm ON tm.team_id = tr.team_id`
|
||||
|
||||
// basicRoleAssignsSQL is a query to select all users basic role (Admin, Editor, Viewer, None) assignments.
|
||||
@@ -63,11 +63,9 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
|
||||
` + filter
|
||||
|
||||
if len(query.RolePrefixes) > 0 {
|
||||
q += " WHERE ( " + strings.Repeat("role.name LIKE ? OR ", len(query.RolePrefixes)-1)
|
||||
q += "role.name LIKE ? )"
|
||||
for i := range query.RolePrefixes {
|
||||
params = append(params, query.RolePrefixes[i]+"%")
|
||||
}
|
||||
rolePrefixesFilter, filterParams := accesscontrol.RolePrefixesFilter(query.RolePrefixes)
|
||||
q += rolePrefixesFilter
|
||||
params = append(params, filterParams...)
|
||||
}
|
||||
|
||||
if err := sess.SQL(q, params...).Find(&result); err != nil {
|
||||
@@ -80,6 +78,82 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) GetBasicRolesPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) {
|
||||
return s.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
Roles: query.Roles,
|
||||
OrgID: query.OrgID,
|
||||
RolePrefixes: query.RolePrefixes,
|
||||
})
|
||||
}
|
||||
|
||||
type teamPermission struct {
|
||||
TeamID int64 `xorm:"team_id"`
|
||||
Action string
|
||||
Scope string
|
||||
}
|
||||
|
||||
func (p teamPermission) Permission() accesscontrol.Permission {
|
||||
return accesscontrol.Permission{
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) GetTeamsPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) (map[int64][]accesscontrol.Permission, error) {
|
||||
teams := query.TeamIDs
|
||||
orgID := query.OrgID
|
||||
rolePrefixes := query.RolePrefixes
|
||||
result := make([]teamPermission, 0)
|
||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
if len(teams) == 0 {
|
||||
// no permission to fetch
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
permission.action,
|
||||
permission.scope,
|
||||
all_role.team_id
|
||||
FROM permission
|
||||
INNER JOIN role ON role.id = permission.role_id
|
||||
INNER JOIN (
|
||||
SELECT tr.role_id, tr.team_id FROM team_role as tr
|
||||
WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teams)-1) + `)
|
||||
AND tr.org_id = ?
|
||||
) as all_role ON role.id = all_role.role_id
|
||||
`
|
||||
|
||||
params := make([]any, 0)
|
||||
for _, team := range teams {
|
||||
params = append(params, team)
|
||||
}
|
||||
params = append(params, orgID)
|
||||
|
||||
if len(rolePrefixes) > 0 {
|
||||
rolePrefixesFilter, filterParams := accesscontrol.RolePrefixesFilter(rolePrefixes)
|
||||
q += rolePrefixesFilter
|
||||
params = append(params, filterParams...)
|
||||
}
|
||||
|
||||
if err := sess.SQL(q, params...).Find(&result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
teamPermissions := make(map[int64][]accesscontrol.Permission)
|
||||
for _, teamPermission := range result {
|
||||
tp := teamPermissions[teamPermission.TeamID]
|
||||
if tp == nil {
|
||||
tp = make([]accesscontrol.Permission, 0)
|
||||
}
|
||||
teamPermissions[teamPermission.TeamID] = append(tp, teamPermission.Permission())
|
||||
}
|
||||
return teamPermissions, err
|
||||
}
|
||||
|
||||
// SearchUsersPermissions returns the list of user permissions in specific organization indexed by UserID
|
||||
func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
||||
type UserRBACPermission struct {
|
||||
|
||||
@@ -162,6 +162,82 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type getTeamsPermissionsTestCase struct {
|
||||
desc string
|
||||
orgID int64
|
||||
teamsPermissions [][]string
|
||||
teamsToQuery []int
|
||||
expected int
|
||||
}
|
||||
|
||||
func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
|
||||
tests := []getTeamsPermissionsTestCase{
|
||||
{
|
||||
desc: "should successfully get team permissions",
|
||||
orgID: 1,
|
||||
teamsPermissions: [][]string{
|
||||
{"100", "2"},
|
||||
{"101", "3"},
|
||||
},
|
||||
teamsToQuery: []int{0, 1},
|
||||
expected: 4,
|
||||
},
|
||||
{
|
||||
desc: "Should not get permissions for teams not listed in the query",
|
||||
orgID: 1,
|
||||
teamsPermissions: [][]string{
|
||||
{"100", "2"},
|
||||
{"101", "3"},
|
||||
},
|
||||
teamsToQuery: []int{0},
|
||||
expected: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
store, permissionStore, _, teamSvc, _ := setupTestEnv(t)
|
||||
|
||||
teams := make([]team.Team, 0)
|
||||
for i := 0; i < len(tt.teamsPermissions); i++ {
|
||||
team, err := teamSvc.CreateTeam(context.Background(), fmt.Sprintf("team-%v", i), "", tt.orgID)
|
||||
require.NoError(t, err)
|
||||
teams = append(teams, team)
|
||||
}
|
||||
|
||||
for teamIDx, teamPermissions := range tt.teamsPermissions {
|
||||
for _, id := range teamPermissions {
|
||||
team := teams[teamIDx]
|
||||
_, err := permissionStore.SetTeamResourcePermission(context.Background(), tt.orgID, team.ID, rs.SetResourcePermissionCommand{
|
||||
Actions: []string{"dashboards:read"},
|
||||
Resource: "dashboards",
|
||||
ResourceID: id,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
teamIDs := make([]int64, 0)
|
||||
for _, teamIDx := range tt.teamsToQuery {
|
||||
if teamIDx < len(teams) {
|
||||
teamIDs = append(teamIDs, teams[teamIDx].ID)
|
||||
}
|
||||
}
|
||||
|
||||
teamsPermissions, err := store.GetTeamsPermissions(context.Background(), accesscontrol.GetUserPermissionsQuery{
|
||||
TeamIDs: teamIDs,
|
||||
OrgID: tt.orgID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
permissions := make([]accesscontrol.Permission, 0)
|
||||
for _, teamPermissions := range teamsPermissions {
|
||||
permissions = append(permissions, teamPermissions...)
|
||||
}
|
||||
assert.Len(t, permissions, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
|
||||
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
|
||||
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
|
||||
|
||||
@@ -177,3 +177,18 @@ func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (stri
|
||||
|
||||
return "INNER JOIN (" + builder.String() + ") as all_role ON role.id = all_role.role_id", params
|
||||
}
|
||||
|
||||
func RolePrefixesFilter(rolePrefixes []string) (string, []any) {
|
||||
query := ""
|
||||
params := make([]any, 0)
|
||||
|
||||
if len(rolePrefixes) > 0 {
|
||||
query += " WHERE ( " + strings.Repeat("role.name LIKE ? OR ", len(rolePrefixes)-1)
|
||||
query += "role.name LIKE ? )"
|
||||
for i := range rolePrefixes {
|
||||
params = append(params, rolePrefixes[i]+"%")
|
||||
}
|
||||
}
|
||||
|
||||
return query, params
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
|
||||
}
|
||||
logger := log.New("extsvcaccounts.test")
|
||||
env.S = &ExtSvcAccountsService{
|
||||
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
||||
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest()),
|
||||
features: fmgt,
|
||||
logger: logger,
|
||||
metrics: newMetrics(nil, env.SaSvc, logger),
|
||||
|
||||
Reference in New Issue
Block a user