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:
Alexander Zobnin
2024-05-07 15:23:11 +02:00
committed by GitHub
parent ee2f6a7b49
commit 82dea4b3e5
11 changed files with 468 additions and 42 deletions
+200 -19
View File
@@ -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
}
}
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()
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"])
})