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:
@@ -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"])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user