The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/accesscontrol/acimpl/service.go

816 lines
27 KiB

package acimpl
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"
Zanzana: Initial dashboard search (#93093) * Zanzana: Search in a background and compare results * refactor * Search with check * instrument zanzana client * add single_read option * refactor * refactor move check into separate function * Fix tests * refactor * refactor getFindDashboardsFn * add resource type to span attributes * run ListObjects concurrently * Use list and search in less cases * adjust metrics buckets * refactor: move Check and ListObjects to AccessControl implementation * Revert "Fix tests" This reverts commit b0c2f072a25029905fdbd26625fdc7a243d4a308. * refactor: use own types for Check and ListObjects inside accesscontrol package * Fix search scenario with low limit and empty query string * more accurate search with checks * revert * fix linter * Revert "revert" This reverts commit ee5f14eea8c2f69e0b59f4a5094d708ac58b0169. * add search errors metric * fix query performance under some conditions * simplify check strategy * fix pagination * refactor findDashboardsZanzanaList * Iterate over multiple pages while making check request * refactor listUserResources * avoid unnecessary db call * remove unused zclient * Add notes for SkipAccessControlFilter * use more accurate check loop * always use check for search with provided UIDs * rename single_read to zanzana_only_evaluation * refactor * update go workspace * fix linter * don't use deprecated fields * refactor * fail if no org specified * refactor * initial integration tests * Fix tests * fix linter errors * fix linter * Fix tests * review suggestions Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * fix limit * refactor * refactor tests * fix db config in tests * fix migrator (postgres) --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
10 months ago
"github.com/grafana/authlib/claims"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"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/metrics"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/infra/tracing"
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
Folders: Show dashboards and folders with directly assigned permissions in "Shared" folder (#78465) * Folders: Show folders user has access to at the root level * Refactor * Refactor * Hide parent folders user has no access to * Skip expensive computation if possible * Fix tests * Fix potential nil access * Fix duplicated folders * Fix linter error * Fix querying folders if no managed permissions set * Update benchmark * Add special shared with me folder and fetch available non-root folders on demand * Fix parents query * Improve db query for folders * Reset benchmark changes * Fix permissions for shared with me folder * Simplify dedup * Add option to include shared folder permission to user's permissions * Fix nil UID * Remove duplicated folders from shared list * Folders: Fix fetching empty folder * Nested folders: Show dashboards with directly assigned permissions * Fix slow dashboards fetch * Refactor * Fix cycle dependencies * Move shared folder to models * Fix shared folder links * Refactor * Use feature flag for permissions * Use feature flag * Review comments * Expose shared folder UID through frontend settings * Add frontend type for sharedWithMeFolderUID option * Refactor: apply review suggestions * Fix parent uid for shared folder * Fix listing shared dashboards for users with access to all folders * Prevent creating folder with "shared" UID * Add tests for shared folders * Add test for shared dashboards * Fix linter * Add metrics for shared with me folder * Add metrics for shared with me dashboards * Fix tests * Tests: add metrics as a dependency * Fix access control metadata for shared with me folder * Use constant for shared with me * Optimize parent folders access check, fetch all folders in one query. * Use labels for metrics
2 years ago
"github.com/grafana/grafana/pkg/services/dashboards"
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
"github.com/grafana/grafana/pkg/services/featuremgmt"
Folders: Show dashboards and folders with directly assigned permissions in "Shared" folder (#78465) * Folders: Show folders user has access to at the root level * Refactor * Refactor * Hide parent folders user has no access to * Skip expensive computation if possible * Fix tests * Fix potential nil access * Fix duplicated folders * Fix linter error * Fix querying folders if no managed permissions set * Update benchmark * Add special shared with me folder and fetch available non-root folders on demand * Fix parents query * Improve db query for folders * Reset benchmark changes * Fix permissions for shared with me folder * Simplify dedup * Add option to include shared folder permission to user's permissions * Fix nil UID * Remove duplicated folders from shared list * Folders: Fix fetching empty folder * Nested folders: Show dashboards with directly assigned permissions * Fix slow dashboards fetch * Refactor * Fix cycle dependencies * Move shared folder to models * Fix shared folder links * Refactor * Use feature flag for permissions * Use feature flag * Review comments * Expose shared folder UID through frontend settings * Add frontend type for sharedWithMeFolderUID option * Refactor: apply review suggestions * Fix parent uid for shared folder * Fix listing shared dashboards for users with access to all folders * Prevent creating folder with "shared" UID * Add tests for shared folders * Add test for shared dashboards * Fix linter * Add metrics for shared with me folder * Add metrics for shared with me dashboards * Fix tests * Tests: add metrics as a dependency * Fix access control metadata for shared with me folder * Use constant for shared with me * Optimize parent folders access check, fetch all folders in one query. * Use labels for metrics
2 years ago
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
var _ pluginaccesscontrol.RoleRegistry = &Service{}
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
const (
cacheTTL = 60 * time.Second
)
Folders: Show dashboards and folders with directly assigned permissions in "Shared" folder (#78465) * Folders: Show folders user has access to at the root level * Refactor * Refactor * Hide parent folders user has no access to * Skip expensive computation if possible * Fix tests * Fix potential nil access * Fix duplicated folders * Fix linter error * Fix querying folders if no managed permissions set * Update benchmark * Add special shared with me folder and fetch available non-root folders on demand * Fix parents query * Improve db query for folders * Reset benchmark changes * Fix permissions for shared with me folder * Simplify dedup * Add option to include shared folder permission to user's permissions * Fix nil UID * Remove duplicated folders from shared list * Folders: Fix fetching empty folder * Nested folders: Show dashboards with directly assigned permissions * Fix slow dashboards fetch * Refactor * Fix cycle dependencies * Move shared folder to models * Fix shared folder links * Refactor * Use feature flag for permissions * Use feature flag * Review comments * Expose shared folder UID through frontend settings * Add frontend type for sharedWithMeFolderUID option * Refactor: apply review suggestions * Fix parent uid for shared folder * Fix listing shared dashboards for users with access to all folders * Prevent creating folder with "shared" UID * Add tests for shared folders * Add test for shared dashboards * Fix linter * Add metrics for shared with me folder * Add metrics for shared with me dashboards * Fix tests * Tests: add metrics as a dependency * Fix access control metadata for shared with me folder * Use constant for shared with me * Optimize parent folders access check, fetch all folders in one query. * Use labels for metrics
2 years ago
var SharedWithMeFolderPermission = accesscontrol.Permission{
Action: dashboards.ActionFoldersRead,
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.SharedWithMeFolderUID),
}
var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix}
func ProvideService(
Revert read replica POC (#93551) * Revert "chore: add replDB to team service (#91799)" This reverts commit c6ae2d7999aa6fc797db39e9d66c6fea70278f83. * Revert "experiment: use read replica for Get and Find Dashboards (#91706)" This reverts commit 54177ca619dbb5ded2dcb158405802d8dbdbc982. * Revert "QuotaService: refactor to use ReplDB for Get queries (#91333)" This reverts commit 299c142f6a6e8c5673cfdea9f87b56ac304f9834. * Revert "refactor replCfg to look more like plugins/plugin config (#91142)" This reverts commit ac0b4bb34d495914cbe8daad85b7c75c31e8070d. * Revert "chore (replstore): fix registration with multiple sql drivers, again (#90990)" This reverts commit daedb358dded00d349d9fac6106aaaa6bf18322e. * Revert "Chore (sqlstore): add validation and testing for repl config (#90683)" This reverts commit af19f039b62d9945377292a8e679ee258fd56b3d. * Revert "ReplStore: Add support for round robin load balancing between multiple read replicas (#90530)" This reverts commit 27b52b1507f5218a7b38046b4d96bc004d949d46. * Revert "DashboardStore: Use ReplDB and get dashboard quotas from the ReadReplica (#90235)" This reverts commit 8a6107cd35f6444c0674ee4230d3d6bcfbbd4a58. * Revert "accesscontrol service read replica (#89963)" This reverts commit 77a4869fcadf13827d76d5767d4de74812d6dd6d. * Revert "Fix: add mapping for the new mysqlRepl driver (#89551)" This reverts commit ab5a079bcc5b0f0a6929f0a3742eb2859d4a3498. * Revert "fix: sql instrumentation dual registration error (#89508)" This reverts commit d988f5c3b064fade6e96511e0024190c22d48e50. * Revert "Experimental Feature Toggle: databaseReadReplica (#89232)" This reverts commit 50244ed4a1435cbf3e3c87d4af34fd7937f7c259.
10 months ago
cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client, permRegistry permreg.PermissionRegistry,
lock *serverlock.ServerLockService,
) (*Service, error) {
service := ProvideOSSService(
cfg,
database.ProvideService(db),
actionResolver,
cache,
features,
tracer,
zclient,
db,
permRegistry,
lock,
)
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
return nil, err
}
// Migrating scopes that haven't been split yet to have kind, attribute and identifier in the DB
// This will be removed once we've:
// 1) removed the feature toggle and
// 2) have released enough versions not to support a version without split scopes
if err := migrator.MigrateScopeSplit(db, service.log); err != nil {
return nil, err
}
return service, nil
}
func ProvideOSSService(
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer,
zclient zanzana.Client, db db.DB, permRegistry permreg.PermissionRegistry, lock *serverlock.ServerLockService,
) *Service {
s := &Service{
actionResolver: actionResolver,
cache: cache,
cfg: cfg,
features: features,
log: log.New("accesscontrol.service"),
roles: accesscontrol.BuildBasicRoleDefinitions(),
store: store,
reconciler: dualwrite.NewZanzanaReconciler(cfg, zclient, db, lock),
permRegistry: permRegistry,
}
return s
}
// Service is the service implementing role based access control.
type Service struct {
actionResolver accesscontrol.ActionResolver
cache *localcache.CacheService
cfg *setting.Cfg
features featuremgmt.FeatureToggles
log log.Logger
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
store accesscontrol.Store
reconciler *dualwrite.ZanzanaReconciler
permRegistry permreg.PermissionRegistry
}
// Run implements accesscontrol.Service.
func (s *Service) Run(ctx context.Context) error {
if s.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return s.reconciler.Reconcile(ctx)
}
return nil
}
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
return map[string]any{
"stats.oss.accesscontrol.enabled.count": 1,
}
}
// 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 := tracer.Start(ctx, "accesscontrol.acimpl.GetUserPermissions")
defer span.End()
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
if !s.cfg.RBAC.PermissionCache || !user.HasUniqueId() {
return s.getUserPermissions(ctx, user, options)
}
return s.getCachedUserPermissions(ctx, user, options)
}
func (s *Service) getUserPermissions(ctx context.Context, user identity.Requester, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getUserPermissions")
defer span.End()
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range accesscontrol.GetOrgRoles(user) {
if basicRole, ok := s.roles[builtin]; ok {
permissions = append(permissions, basicRole.Permissions...)
}
}
Folders: Show dashboards and folders with directly assigned permissions in "Shared" folder (#78465) * Folders: Show folders user has access to at the root level * Refactor * Refactor * Hide parent folders user has no access to * Skip expensive computation if possible * Fix tests * Fix potential nil access * Fix duplicated folders * Fix linter error * Fix querying folders if no managed permissions set * Update benchmark * Add special shared with me folder and fetch available non-root folders on demand * Fix parents query * Improve db query for folders * Reset benchmark changes * Fix permissions for shared with me folder * Simplify dedup * Add option to include shared folder permission to user's permissions * Fix nil UID * Remove duplicated folders from shared list * Folders: Fix fetching empty folder * Nested folders: Show dashboards with directly assigned permissions * Fix slow dashboards fetch * Refactor * Fix cycle dependencies * Move shared folder to models * Fix shared folder links * Refactor * Use feature flag for permissions * Use feature flag * Review comments * Expose shared folder UID through frontend settings * Add frontend type for sharedWithMeFolderUID option * Refactor: apply review suggestions * Fix parent uid for shared folder * Fix listing shared dashboards for users with access to all folders * Prevent creating folder with "shared" UID * Add tests for shared folders * Add test for shared dashboards * Fix linter * Add metrics for shared with me folder * Add metrics for shared with me dashboards * Fix tests * Tests: add metrics as a dependency * Fix access control metadata for shared with me folder * Use constant for shared with me * Optimize parent folders access check, fetch all folders in one query. * Use labels for metrics
2 years ago
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, SharedWithMeFolderPermission)
}
// we don't care about the error here, if this fails we get 0 and no
// permission assigned to user will be returned, only for org role.
userID, _ := identity.UserIdentifier(user.GetID())
dbPermissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
OrgID: user.GetOrgID(),
UserID: userID,
Roles: accesscontrol.GetOrgRoles(user),
TeamIDs: user.GetTeams(),
RolePrefixes: OSSRolesPrefixes,
})
if err != nil {
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
}
return append(permissions, dbPermissions...), nil
}
func (s *Service) getBasicRolePermissions(ctx context.Context, role string, orgID int64) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getBasicRolePermissions")
defer span.End()
var permissions []accesscontrol.Permission
if basicRole, ok := s.roles[role]; ok {
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,
})
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
}
return append(permissions, dbPermissions...), err
}
func (s *Service) getTeamsPermissions(ctx context.Context, teamIDs []int64, orgID int64) (map[int64][]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getTeamsPermissions")
defer span.End()
teamPermissions, err := s.store.GetTeamsPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
TeamIDs: teamIDs,
OrgID: orgID,
RolePrefixes: OSSRolesPrefixes,
})
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for teamID, permissions := range teamPermissions {
teamPermissions[teamID] = s.actionResolver.ExpandActionSets(permissions)
}
}
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 := tracer.Start(ctx, "accesscontrol.acimpl.getUserDirectPermissions")
defer span.End()
var userID int64
if user.IsIdentityType(claims.TypeUser, claims.TypeServiceAccount) {
var err error
userID, err = user.GetInternalID()
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.FlagAccessActionSets) {
permissions = s.actionResolver.ExpandActionSets(permissions)
}
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) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getCachedUserPermissions")
defer span.End()
cacheKey := accesscontrol.GetUserPermissionCacheKey(user)
if cachedPermissions, ok := s.cache.Get(cacheKey); ok {
return cachedPermissions.([]accesscontrol.Permission), nil
}
permissions, 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
}
permissions = append(permissions, teamsPermissions...)
userManagedPermissions, err := s.getCachedUserDirectPermissions(ctx, user, options)
if err != nil {
return nil, err
}
permissions = append(permissions, userManagedPermissions...)
s.cache.Set(cacheKey, permissions, cacheTTL)
span.SetAttributes(attribute.Int("num_permissions", len(permissions)))
return permissions, nil
}
func (s *Service) getCachedBasicRolesPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getCachedBasicRolesPermissions")
defer span.End()
// Viewer role has ~30 permissions, so we can pre-allocate memory
permissions := make([]accesscontrol.Permission, 0, 50)
basicRoles := accesscontrol.GetOrgRoles(user)
span.SetAttributes(attribute.Int("roles", len(basicRoles)))
for _, role := range basicRoles {
perms, err := s.getCachedBasicRolePermissions(ctx, role, user.GetOrgID(), options)
span.SetAttributes(attribute.Int(fmt.Sprintf("role_%s_permissions", role), len(perms)))
if err != nil {
return nil, err
}
permissions = append(permissions, perms...)
}
return permissions, nil
}
func (s *Service) getCachedBasicRolePermissions(ctx context.Context, role string, orgID int64, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getCachedBasicRolePermissions")
defer span.End()
key := accesscontrol.GetBasicRolePermissionCacheKey(role, orgID)
getPermissionsFn := func(ctx context.Context) ([]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 := tracer.Start(ctx, "accesscontrol.acimpl.getCachedUserDirectPermissions")
defer span.End()
key := accesscontrol.GetUserDirectPermissionCacheKey(user)
getUserPermissionsFn := func(ctx context.Context) ([]accesscontrol.Permission, error) {
return s.getUserDirectPermissions(ctx, user)
}
return s.getCachedPermissions(ctx, key, getUserPermissionsFn, options)
}
type getPermissionsFunc = func(ctx context.Context) ([]accesscontrol.Permission, error)
// Generic method for getting various permissions from cache
func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermissionsFn getPermissionsFunc, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.getCachedPermissions")
defer span.End()
if !options.ReloadCache {
permissions, ok := s.cache.Get(key)
if ok {
span.SetAttributes(attribute.Int("num_permissions_cached", len(permissions.([]accesscontrol.Permission))))
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
return permissions.([]accesscontrol.Permission), nil
}
}
span.AddEvent("cache miss")
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
permissions, err := getPermissionsFn(ctx)
span.SetAttributes(attribute.Int("num_permissions_fetched", len(permissions)))
if err != nil {
return nil, err
}
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 := tracer.Start(ctx, "accesscontrol.acimpl.getCachedTeamsPermissions")
defer span.End()
teams := user.GetTeams()
orgID := user.GetOrgID()
if len(teams) == 0 {
return []accesscontrol.Permission{}, nil
}
miss := make([]int64, 0)
permissions := make([]accesscontrol.Permission, 0)
if !options.ReloadCache {
for _, teamID := range teams {
key := accesscontrol.GetTeamPermissionCacheKey(teamID, orgID)
teamPermissions, ok := s.cache.Get(key)
if ok {
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
span.SetAttributes(attribute.Int("num_permissions_cached", len(teamPermissions.([]accesscontrol.Permission))))
permissions = append(permissions, teamPermissions.([]accesscontrol.Permission)...)
} else {
miss = append(miss, teamID)
}
}
} else {
// reload cache and fetch all teams permissions
miss = teams
}
if len(miss) > 0 {
span.AddEvent("cache miss")
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
teamsPermissions, err := s.getTeamsPermissions(ctx, miss, orgID)
span.SetAttributes(attribute.Int("num_permissions_fetched", len(teamsPermissions)))
if err != nil {
return nil, err
}
for teamID, teamPermissions := range teamsPermissions {
key := accesscontrol.GetTeamPermissionCacheKey(teamID, orgID)
s.cache.Set(key, teamPermissions, cacheTTL)
permissions = append(permissions, teamPermissions...)
}
}
return permissions, nil
}
func (s *Service) ClearUserPermissionCache(user identity.Requester) {
s.cache.Delete(accesscontrol.GetUserPermissionCacheKey(user))
s.cache.Delete(accesscontrol.GetUserDirectPermissionCacheKey(user))
}
func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteUserPermissions")
defer span.End()
return s.store.DeleteUserPermissions(ctx, orgID, userID)
}
func (s *Service) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteTeamPermissions")
defer span.End()
return s.store.DeleteTeamPermissions(ctx, orgID, teamID)
}
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
for _, r := range registrations {
err := accesscontrol.ValidateFixedRole(r.Role)
if err != nil {
return err
}
err = accesscontrol.ValidateBuiltInRoles(r.Grants)
if err != nil {
return err
}
for i := range r.Role.Permissions {
if err := s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope); err != nil {
return err
}
}
s.registrations.Append(r)
}
return nil
}
// RegisterFixedRoles registers all declared roles in RAM
func (s *Service) RegisterFixedRoles(ctx context.Context) error {
_, span := tracer.Start(ctx, "accesscontrol.acimpl.RegisterFixedRoles")
defer span.End()
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
if basicRole, ok := s.roles[br]; ok {
basicRole.Permissions = append(basicRole.Permissions, registration.Role.Permissions...)
} else {
s.log.Error("Unknown builtin role", "builtInRole", br)
}
}
return true
})
return nil
}
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
// 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 {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeclarePluginRoles")
defer span.End()
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
// Protect behind feature toggle
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
return nil
}
acRegs := pluginutils.ToRegistrations(ID, name, regs)
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
for _, r := range acRegs {
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
return err
}
if err := accesscontrol.ValidateBuiltInRoles(r.Grants); err != nil {
return err
}
for i := range r.Role.Permissions {
// Register plugin actions and their possible scopes for permission validation
s.permRegistry.RegisterPluginScope(r.Role.Permissions[i].Scope)
if err := s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope); err != nil {
return err
}
}
RBAC: Allow role registration for plugins (#57387) * Picking role registration from OnCall POC branch * Fix test * Remove include actions from this PR * Removing unused permission * Adding test to DeclarePluginRoles * Add testcase to RegisterFixed role * Additional test case * Adding tests to validate plugins roles * Add test to plugin loader * Nit. * Scuemata validation * Changing the design to decouple accesscontrol from plugin management Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Fixing tests Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add missing files Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Remove feature toggle check from loader * Remove feature toggleimport * Feedback Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Fix test' * Make plugins.RoleRegistry interface typed * Remove comment question * No need for json tags anymore * Nit. log * Adding the schema validation * Remove group to take plugin Name instead * Revert sqlstore -> db * Nit. * Nit. on tests Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Update pkg/services/accesscontrol/plugins.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Log message Co-Authored-By: marefr <marcus.efraimsson@gmail.com> * Remove unecessary method. Update test name. Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fix linting * Update cue descriptions * Fix test Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> Co-authored-by: marefr <marcus.efraimsson@gmail.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
3 years ago
s.log.Debug("Registering plugin role", "role", r.Role.Name)
s.registrations.Append(r)
}
return nil
}
func GetActionFilter(options accesscontrol.SearchOptions) func(action string) bool {
return func(action string) bool {
if options.ActionPrefix != "" {
return strings.HasPrefix(action, options.ActionPrefix)
} else {
return action == options.Action
}
}
}
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SearchUsersPermissions")
defer span.End()
// Limit roles to available in OSS
options.RolePrefixes = OSSRolesPrefixes
if options.TypedID != "" {
userID, err := options.ComputeUserID()
if err != nil {
s.log.Error("Failed to resolve user ID", "error", err)
return nil, err
}
// Reroute to the user specific implementation of search permissions
// because it leverages the user permission cache.
userPerms, err := s.SearchUserPermissions(ctx, usr.GetOrgID(), options)
if err != nil {
return nil, err
}
return map[int64][]accesscontrol.Permission{userID: userPerms}, nil
}
timer := prometheus.NewTimer(metrics.MAccessSearchPermissionsSummary)
defer timer.ObserveDuration()
// Filter ram permissions
basicPermissions := map[string][]accesscontrol.Permission{}
for role, basicRole := range s.roles {
for i := range basicRole.Permissions {
if PermissionMatchesSearchOptions(basicRole.Permissions[i], &options) {
basicPermissions[role] = append(basicPermissions[role], basicRole.Permissions[i])
}
}
}
usersRoles, err := s.store.GetUsersBasicRoles(ctx, nil, usr.GetOrgID())
if err != nil {
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
options.ActionSets = s.actionResolver.ResolveAction(options.Action)
options.ActionSets = append(options.ActionSets,
s.actionResolver.ResolveActionPrefix(options.ActionPrefix)...)
}
// Get managed permissions (DB)
usersPermissions, err := s.store.SearchUsersPermissions(ctx, usr.GetOrgID(), options)
if err != nil {
return nil, err
}
// helper to filter out permissions the signed in users cannot see
canView := func() func(userID int64) bool {
siuPermissions := usr.GetPermissions()
if len(siuPermissions) == 0 {
return func(_ int64) bool { return false }
}
scopes, ok := siuPermissions[accesscontrol.ActionUsersPermissionsRead]
if !ok {
return func(_ int64) bool { return false }
}
ids := map[int64]bool{}
for i := range scopes {
if strings.HasSuffix(scopes[i], "*") {
return func(_ int64) bool { return true }
}
parts := strings.Split(scopes[i], ":")
if len(parts) != 3 {
continue
}
id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
continue
}
ids[id] = true
}
return func(userID int64) bool { return ids[userID] }
}()
// Merge stored (DB) and basic role permissions (RAM)
// Assumes that all users with stored permissions have org roles
res := map[int64][]accesscontrol.Permission{}
for userID, roles := range usersRoles {
if !canView(userID) {
continue
}
perms := []accesscontrol.Permission{}
for i := range roles {
basicPermission, ok := basicPermissions[roles[i]]
if !ok {
continue
}
perms = append(perms, basicPermission...)
}
if dbPerms, ok := usersPermissions[userID]; ok {
perms = append(perms, dbPerms...)
}
if len(perms) > 0 {
res[userID] = perms
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) && len(options.ActionSets) > 0 {
for id, perms := range res {
res[id] = s.actionResolver.ExpandActionSetsWithFilter(perms, GetActionFilter(options))
}
}
return res, nil
}
func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SearchUserPermissions")
defer span.End()
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
if searchOptions.TypedID == "" {
return nil, fmt.Errorf("expected namespaced ID to be specified")
}
if permissions, success := s.searchUserPermissionsFromCache(ctx, orgID, searchOptions); success {
return permissions, nil
}
return s.searchUserPermissions(ctx, orgID, searchOptions)
}
func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.searchUserPermissions")
defer span.End()
userID, err := searchOptions.ComputeUserID()
if err != nil {
return nil, err
}
// Get permissions for user's basic roles from RAM
roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{userID}, orgID)
if err != nil {
return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err)
}
var roles []string
var ok bool
if roles, ok = roleList[userID]; !ok {
return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", userID, orgID)
}
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range roles {
if basicRole, ok := s.roles[builtin]; ok {
for _, permission := range basicRole.Permissions {
if PermissionMatchesSearchOptions(permission, &searchOptions) {
permissions = append(permissions, permission)
}
}
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
searchOptions.ActionSets = s.actionResolver.ResolveAction(searchOptions.Action)
searchOptions.ActionSets = append(searchOptions.ActionSets,
s.actionResolver.ResolveActionPrefix(searchOptions.ActionPrefix)...)
}
// Get permissions from the DB
dbPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, searchOptions)
if err != nil {
return nil, err
}
permissions = append(permissions, dbPermissions[userID]...)
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) && len(searchOptions.ActionSets) != 0 {
permissions = s.actionResolver.ExpandActionSetsWithFilter(permissions, GetActionFilter(searchOptions))
}
key, err := accesscontrol.GetSearchPermissionCacheKey(s.log, &user.SignedInUser{UserID: userID, OrgID: orgID}, searchOptions)
if err != nil {
s.log.Warn("failed to create search permission cache key", "err", err)
} else {
s.cache.Set(key, permissions, cacheTTL)
}
return permissions, nil
}
func (s *Service) searchUserPermissionsFromCache(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
_, span := tracer.Start(ctx, "accesscontrol.acimpl.searchUserPermissionsFromCache")
defer span.End()
userID, err := searchOptions.ComputeUserID()
if err != nil {
return nil, false
}
// Create a temp signed in user object to retrieve cache key
tempUser := &user.SignedInUser{
UserID: userID,
OrgID: orgID,
}
key, err := accesscontrol.GetSearchPermissionCacheKey(s.log, tempUser, searchOptions)
if err != nil {
s.log.Warn("failed to create search permission cache key", "err", err)
return nil, false
}
permissions, ok := s.cache.Get((key))
if !ok {
metrics.MAccessSearchUserPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
return nil, false
}
s.log.Debug("Using cached permissions", "key", key)
metrics.MAccessSearchUserPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
return permissions.([]accesscontrol.Permission), true
}
func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchOptions *accesscontrol.SearchOptions) bool {
if searchOptions.Scope != "" {
// Permissions including the scope should also match
scopes := append(searchOptions.Wildcards(), searchOptions.Scope)
if !slices.Contains[[]string, string](scopes, permission.Scope) {
return false
}
}
if searchOptions.Action != "" {
return permission.Action == searchOptions.Action
}
return strings.HasPrefix(permission.Action, searchOptions.ActionPrefix)
}
func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole")
defer span.End()
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.")
return nil
}
if err := cmd.Validate(); err != nil {
return err
}
return s.store.SaveExternalServiceRole(ctx, cmd)
}
func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole")
defer span.End()
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.")
return nil
}
slug := slugify.Slugify(externalServiceID)
return s.store.DeleteExternalServiceRole(ctx, slug)
}
func (*Service) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol.SyncUserRolesCommand) error {
return nil
}
func (s *Service) GetRoleByName(ctx context.Context, orgID int64, roleName string) (*accesscontrol.RoleDTO, error) {
_, span := tracer.Start(ctx, "accesscontrol.acimpl.GetRoleByName")
defer span.End()
err := accesscontrol.ErrRoleNotFound
if _, ok := s.roles[roleName]; ok {
return nil, err
}
var role *accesscontrol.RoleDTO
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
if registration.Role.Name == roleName {
role = &accesscontrol.RoleDTO{
Name: registration.Role.Name,
Permissions: registration.Role.Permissions,
DisplayName: registration.Role.DisplayName,
Description: registration.Role.Description,
}
err = nil
return false
}
return true
})
return role, err
}