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
pull/87437/head
Alexander Zobnin 1 year ago committed by GitHub
parent ee2f6a7b49
commit 82dea4b3e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/api/folder_bench_test.go
  2. 8
      pkg/cmd/grafana-cli/commands/conflict_user_command.go
  3. 2
      pkg/services/accesscontrol/accesscontrol.go
  4. 205
      pkg/services/accesscontrol/acimpl/service.go
  5. 2
      pkg/services/accesscontrol/acimpl/service_test.go
  6. 18
      pkg/services/accesscontrol/actest/fake.go
  7. 80
      pkg/services/accesscontrol/actest/store_mock.go
  8. 86
      pkg/services/accesscontrol/database/database.go
  9. 76
      pkg/services/accesscontrol/database/database_test.go
  10. 15
      pkg/services/accesscontrol/filter.go
  11. 2
      pkg/services/serviceaccounts/extsvcaccounts/service_test.go

@ -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) 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
}
}
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()
s.log.Debug("Using cached permissions", "key", key)
return permissions.([]accesscontrol.Permission), nil
}
}
span.AddEvent("cache miss")
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
s.log.Debug("Fetch permissions from store", "key", key)
permissions, err := s.getUserPermissions(ctx, user, options)
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),

Loading…
Cancel
Save