package rbac import ( "context" "fmt" "strconv" "strings" "time" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/singleflight" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/apiserver/pkg/endpoints/request" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/cache" "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/storage/legacysql" ) const ( shortCacheTTL = 30 * time.Second longCacheTTL = 2 * time.Minute ) type Service struct { authzv1.UnimplementedAuthzServiceServer store store.Store folderStore store.FolderStore permissionStore store.PermissionStore identityStore legacy.LegacyIdentityStore settings Settings mapper MapperRegistry logger log.Logger tracer tracing.Tracer metrics *metrics // Deduplication of concurrent requests sf *singleflight.Group // Cache for user permissions, user team memberships and user basic roles idCache cacheWrap[store.UserIdentifiers] permCache cacheWrap[map[string]bool] permDenialCache cacheWrap[bool] userTeamCache cacheWrap[[]int64] basicRoleCache cacheWrap[store.BasicRole] folderCache cacheWrap[folderTree] teamIDCache cacheWrap[map[int64]string] } type Settings struct { AnonOrgRole string // CacheTTL is the time to live for the permission cache entries. // Set to 0 to disable caching. CacheTTL time.Duration } func NewService( sql legacysql.LegacyDatabaseProvider, folderStore store.FolderStore, identityStore legacy.LegacyIdentityStore, permissionStore store.PermissionStore, logger log.Logger, tracer tracing.Tracer, reg prometheus.Registerer, cache cache.Cache, settings Settings, ) *Service { if settings.AnonOrgRole == "" { settings.AnonOrgRole = "Viewer" } return &Service{ store: store.NewStore(sql, tracer), folderStore: folderStore, permissionStore: permissionStore, identityStore: identityStore, settings: settings, logger: logger, tracer: tracer, metrics: newMetrics(reg), mapper: NewMapperRegistry(), idCache: newCacheWrap[store.UserIdentifiers](cache, logger, tracer, longCacheTTL), permCache: newCacheWrap[map[string]bool](cache, logger, tracer, settings.CacheTTL), permDenialCache: newCacheWrap[bool](cache, logger, tracer, settings.CacheTTL), userTeamCache: newCacheWrap[[]int64](cache, logger, tracer, settings.CacheTTL), basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, tracer, settings.CacheTTL), folderCache: newCacheWrap[folderTree](cache, logger, tracer, settings.CacheTTL), teamIDCache: newCacheWrap[map[int64]string](cache, logger, tracer, longCacheTTL), sf: new(singleflight.Group), } } func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.Check") defer span.End() ctxLogger := s.logger.FromContext(ctx).New( "subject", req.GetSubject(), "namespace", req.GetNamespace(), "group", req.GetGroup(), "resource", req.GetResource(), "verb", req.GetVerb(), ) defer func(start time.Time) { ctxLogger.Debug("Check execution time", "duration", time.Since(start).Milliseconds()) }(time.Now()) allow := &authzv1.CheckResponse{Allowed: true} deny := &authzv1.CheckResponse{Allowed: false} checkReq, err := s.validateCheckRequest(ctx, req) if err != nil { ctxLogger.Error("invalid request", "error", err) s.metrics.requestCount.WithLabelValues("true", "false", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return deny, err } ctx = request.WithNamespace(ctx, req.GetNamespace()) span.SetAttributes( attribute.String("subject", req.Subject), attribute.String("namespace", checkReq.Namespace.Value), attribute.String("action", checkReq.Action), attribute.String("name", checkReq.Name), attribute.String("folder", checkReq.ParentFolder), attribute.Bool("allowed", false), ) permDenialKey := userPermDenialCacheKey(checkReq.Namespace.Value, checkReq.UserUID, checkReq.Action, checkReq.Name, checkReq.ParentFolder) if _, ok := s.permDenialCache.Get(ctx, permDenialKey); ok { s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc() s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return deny, nil } cachedPerms, err := s.getCachedIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action) if err == nil { allowed, err := s.checkPermission(ctx, cachedPerms, checkReq) if err != nil { ctxLogger.Error("could not check permission", "error", err) s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return deny, err } if allowed { s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc() s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() span.SetAttributes(attribute.Bool("allowed", true)) return allow, nil } } s.metrics.permissionCacheUsage.WithLabelValues("false", checkReq.Action).Inc() permissions, err := s.getIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action, checkReq.ActionSets) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return deny, err } allowed, err := s.checkPermission(ctx, permissions, checkReq) if err != nil { ctxLogger.Error("could not check permission", "error", err) s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return deny, err } if !allowed { s.permDenialCache.Set(ctx, permDenialKey, true) } s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() span.SetAttributes(attribute.Bool("allowed", allowed)) return &authzv1.CheckResponse{Allowed: allowed}, nil } func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1.ListResponse, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.List") defer span.End() ctxLogger := s.logger.FromContext(ctx).New( "subject", req.GetSubject(), "namespace", req.GetNamespace(), "group", req.GetGroup(), "resource", req.GetResource(), "verb", req.GetVerb(), ) defer func(start time.Time) { ctxLogger.Debug("List execution time", "duration", time.Since(start).Milliseconds()) }(time.Now()) listReq, err := s.validateListRequest(ctx, req) if err != nil { ctxLogger.Error("invalid request", "error", err) s.metrics.requestCount.WithLabelValues("true", "false", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return &authzv1.ListResponse{}, err } ctx = request.WithNamespace(ctx, req.GetNamespace()) span.SetAttributes( attribute.String("subject", req.Subject), attribute.String("namespace", listReq.Namespace.Value), attribute.String("action", listReq.Action), ) var permissions map[string]bool cacheHit := false if !listReq.Options.SkipCache { permissions, err = s.getCachedIdentityPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action) if err == nil { s.metrics.permissionCacheUsage.WithLabelValues("true", listReq.Action).Inc() cacheHit = true } } if err != nil || listReq.Options.SkipCache { s.metrics.permissionCacheUsage.WithLabelValues("false", listReq.Action).Inc() permissions, err = s.getIdentityPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action, listReq.ActionSets) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() return nil, err } } resp, err := s.listPermission(ctx, permissions, listReq) s.metrics.requestCount.WithLabelValues(strconv.FormatBool(err != nil), "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc() if err != nil { return nil, err } if resp != nil && resp.Zookie != nil { if cacheHit && time.Duration(time.Now().Unix()-resp.Zookie.Timestamp) < s.settings.CacheTTL { resp.Zookie = &authzv1.Zookie{Timestamp: time.Now().Add(-s.settings.CacheTTL).Unix()} } } return resp, nil } func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*checkRequest, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateCheckRequest") defer span.End() ns, err := validateNamespace(ctx, req.GetNamespace()) if err != nil { return nil, err } userUID, idType, err := s.validateSubject(ctx, req.GetSubject()) if err != nil { return nil, err } action, actionSets, err := s.validateAction(ctx, req.GetGroup(), req.GetResource(), req.GetVerb()) if err != nil { return nil, err } checkReq := &checkRequest{ Namespace: ns, UserUID: userUID, IdentityType: idType, Action: action, ActionSets: actionSets, Group: req.GetGroup(), Resource: req.GetResource(), Verb: req.GetVerb(), Name: req.GetName(), ParentFolder: req.GetFolder(), } return checkReq, nil } func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequest) (*listRequest, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateListRequest") defer span.End() ns, err := validateNamespace(ctx, req.GetNamespace()) if err != nil { return nil, err } userUID, idType, err := s.validateSubject(ctx, req.GetSubject()) if err != nil { return nil, err } action, actionSets, err := s.validateAction(ctx, req.GetGroup(), req.GetResource(), req.GetVerb()) if err != nil { return nil, err } authzOptions := req.GetOptions() if authzOptions == nil { authzOptions = &authzv1.ListRequestOptions{} } options := &ListRequestOptions{ SkipCache: authzOptions.Skipcache, } listReq := &listRequest{ Namespace: ns, UserUID: userUID, IdentityType: idType, Action: action, ActionSets: actionSets, Group: req.GetGroup(), Resource: req.GetResource(), Verb: req.GetVerb(), Options: options, } return listReq, nil } func validateNamespace(ctx context.Context, nameSpace string) (types.NamespaceInfo, error) { if nameSpace == "" { return types.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required") } authInfo, has := types.AuthInfoFrom(ctx) if !has { return types.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context") } if !types.NamespaceMatches(authInfo.GetNamespace(), nameSpace) { return types.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match") } ns, err := types.ParseNamespace(nameSpace) if err != nil { return types.NamespaceInfo{}, err } return ns, nil } func (s *Service) validateSubject(ctx context.Context, subject string) (string, types.IdentityType, error) { if subject == "" { return "", "", status.Error(codes.InvalidArgument, "subject is required") } ctxLogger := s.logger.FromContext(ctx) identityType, userUID, err := types.ParseTypeID(subject) if err != nil { return "", "", err } // Permission check currently only checks user, anonymous user, service account and renderer permissions if !types.IsIdentityType(identityType, types.TypeUser, types.TypeServiceAccount, types.TypeAnonymous, types.TypeRenderService) { ctxLogger.Error("unsupported identity type", "type", identityType) return "", "", status.Error(codes.PermissionDenied, "unsupported identity type") } return userUID, identityType, nil } // Find the action for a selected verb func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, []string, error) { ctxLogger := s.logger.FromContext(ctx) t, ok := s.mapper.Get(group, resource) if !ok { ctxLogger.Error("unsupported resource", "group", group, "resource", resource) return "", nil, status.Error(codes.NotFound, "unsupported resource") } action, ok := t.Action(verb) if !ok { ctxLogger.Error("unsupported verb", "group", group, "resource", resource, "verb", verb) return "", nil, status.Error(codes.NotFound, "unsupported verb") } actionSets := t.ActionSets(verb) return action, actionSets, nil } func (s *Service) getIdentityPermissions(ctx context.Context, ns types.NamespaceInfo, idType types.IdentityType, userID, action string, actionSets []string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getIdentityPermissions") defer span.End() switch idType { case types.TypeAnonymous: return s.getAnonymousPermissions(ctx, ns, action, actionSets) case types.TypeRenderService: return s.getRendererPermissions(ctx, action) case types.TypeUser, types.TypeServiceAccount: return s.getUserPermissions(ctx, ns, userID, action, actionSets) default: return nil, fmt.Errorf("unsupported identity type: %s", idType) } } func (s *Service) getCachedIdentityPermissions(ctx context.Context, ns types.NamespaceInfo, idType types.IdentityType, userID, action string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getCachedIdentityPermissions") defer span.End() switch idType { case types.TypeAnonymous: anonPermKey := anonymousPermCacheKey(ns.Value, action) if cached, ok := s.permCache.Get(ctx, anonPermKey); ok { return cached, nil } return nil, cache.ErrNotFound case types.TypeRenderService: return nil, cache.ErrNotFound case types.TypeUser, types.TypeServiceAccount: userIdentifiers, err := s.GetUserIdentifiers(ctx, ns, userID) if err != nil { return nil, err } userPermKey := userPermCacheKey(ns.Value, userIdentifiers.UID, action) if cached, ok := s.permCache.Get(ctx, userPermKey); ok { return cached, nil } return nil, cache.ErrNotFound default: return nil, fmt.Errorf("unsupported identity type: %s", idType) } } func (s *Service) getUserPermissions(ctx context.Context, ns types.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions") defer span.End() userIdentifiers, err := s.GetUserIdentifiers(ctx, ns, userID) if err != nil { return nil, err } userPermKey := userPermCacheKey(ns.Value, userIdentifiers.UID, action) res, err, _ := s.sf.Do(userPermKey+"_getUserPermissions", func() (interface{}, error) { basicRoles, err := s.getUserBasicRole(ctx, ns, userIdentifiers) if err != nil { return nil, err } teamIDs, err := s.getUserTeams(ctx, ns, userIdentifiers) if err != nil { return nil, err } userPermQuery := store.PermissionsQuery{ UserID: userIdentifiers.ID, Action: action, ActionSets: actionSets, TeamIDs: teamIDs, Role: basicRoles.Role, IsServerAdmin: basicRoles.IsAdmin, } permissions, err := s.permissionStore.GetUserPermissions(ctx, ns, userPermQuery) if err != nil { return nil, err } scopeMap := s.getScopeMap(permissions) scopeMap, err = s.resolveScopeMap(ctx, ns, scopeMap) if err != nil { return nil, fmt.Errorf("could not resolve scope map: %w", err) } s.permCache.Set(ctx, userPermKey, scopeMap) span.SetAttributes(attribute.Int("num_permissions_fetched", len(permissions))) return scopeMap, nil }) if err != nil { return nil, err } return res.(map[string]bool), nil } func (s *Service) getAnonymousPermissions(ctx context.Context, ns types.NamespaceInfo, action string, actionSets []string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getAnonymousPermissions") defer span.End() anonPermKey := anonymousPermCacheKey(ns.Value, action) res, err, _ := s.sf.Do(anonPermKey+"_getAnonymousPermissions", func() (interface{}, error) { permissions, err := s.permissionStore.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, ActionSets: actionSets, Role: s.settings.AnonOrgRole}) if err != nil { return nil, err } scopeMap := s.getScopeMap(permissions) s.permCache.Set(ctx, anonPermKey, scopeMap) return scopeMap, nil }) if err != nil { return nil, err } return res.(map[string]bool), nil } // Renderer is granted permissions to read all dashboards and folders, and no other permissions func (s *Service) getRendererPermissions(ctx context.Context, action string) (map[string]bool, error) { _, span := s.tracer.Start(ctx, "authz_direct_db.service.getRendererPermissions") defer span.End() if action == "dashboards:read" || action == "folders:read" || action == "datasources:read" { return map[string]bool{"*": true}, nil } return map[string]bool{}, nil } func (s *Service) GetUserIdentifiers(ctx context.Context, ns types.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { uidCacheKey := userIdentifierCacheKey(ns.Value, userUID) if cached, ok := s.idCache.Get(ctx, uidCacheKey); ok { return &cached, nil } idCacheKey := userIdentifierCacheKeyById(ns.Value, userUID) if cached, ok := s.idCache.Get(ctx, idCacheKey); ok { return &cached, nil } var userIDQuery store.UserIdentifierQuery // Assume that numeric UID is user ID if userID, err := strconv.Atoi(userUID); err == nil { userIDQuery = store.UserIdentifierQuery{UserID: int64(userID)} } else { userIDQuery = store.UserIdentifierQuery{UserUID: userUID} } userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery) if err != nil { return nil, fmt.Errorf("could not get user internal id: %w", err) } s.idCache.Set(ctx, uidCacheKey, *userIdentifiers) s.idCache.Set(ctx, idCacheKey, *userIdentifiers) return userIdentifiers, nil } func (s *Service) getUserTeams(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserTeams") defer span.End() teamIDs := make([]int64, 0, 50) teamsCacheKey := userTeamCacheKey(ns.Value, userIdentifiers.UID) if cached, ok := s.userTeamCache.Get(ctx, teamsCacheKey); ok { return cached, nil } teamQuery := legacy.ListUserTeamsQuery{ UserUID: userIdentifiers.UID, Pagination: common.Pagination{Limit: 50}, } for { teams, err := s.identityStore.ListUserTeams(ctx, ns, teamQuery) if err != nil { return nil, fmt.Errorf("could not get user teams: %w", err) } for _, team := range teams.Items { teamIDs = append(teamIDs, team.ID) } teamQuery.Pagination.Continue = teams.Continue if teams.Continue == 0 { break } } s.userTeamCache.Set(ctx, teamsCacheKey, teamIDs) span.SetAttributes(attribute.Int("num_user_teams", len(teamIDs))) return teamIDs, nil } func (s *Service) getUserBasicRole(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserBasicRole") defer span.End() basicRoleKey := userBasicRoleCacheKey(ns.Value, userIdentifiers.UID) if cached, ok := s.basicRoleCache.Get(ctx, basicRoleKey); ok { return cached, nil } basicRole, err := s.store.GetBasicRoles(ctx, ns, store.BasicRoleQuery{UserID: userIdentifiers.ID}) if err != nil { return store.BasicRole{}, fmt.Errorf("could not get basic roles: %w", err) } if basicRole == nil { basicRole = &store.BasicRole{} } s.basicRoleCache.Set(ctx, basicRoleKey, *basicRole) return *basicRole, nil } func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.checkPermission", trace.WithAttributes( attribute.Int("scope_count", len(scopeMap)))) defer span.End() ctxLogger := s.logger.FromContext(ctx) // Only check action if the request doesn't specify scope if req.Name == "" && req.Verb != utils.VerbCreate { return len(scopeMap) > 0, nil } t, ok := s.mapper.Get(req.Group, req.Resource) if !ok { ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) return false, status.Error(codes.NotFound, "unsupported resource") } if t.SkipScope(req.Verb) { // Resource doesn't require scope on this verb, so allow if the user has the action return scopeMap[""], nil } // If creating a resource that goes in a folder, but no folder is specified, // assume parent folder is the general folder if req.Verb == utils.VerbCreate && t.HasFolderSupport() && req.ParentFolder == "" { req.ParentFolder = accesscontrol.GeneralFolderUID } // Wildcard grant, no further checks needed if scopeMap["*"] { return true, nil } if req.Name != "" && scopeMap[t.Scope(req.Name)] { return true, nil } if !t.HasFolderSupport() { return false, nil } return s.checkInheritedPermissions(ctx, scopeMap, req) } func (s *Service) getScopeMap(permissions []accesscontrol.Permission) map[string]bool { permMap := make(map[string]bool, len(permissions)) for _, perm := range permissions { // We've had cases where the scope wasn't split properly, // failing wildcard checks. This is a recovery mechanism. if perm.Kind == "" && perm.Scope != "" { s.logger.Warn("found unsplit permission scope", "scope", perm.Scope) perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) } // If has any wildcard, return immediately if perm.Kind == "*" || perm.Attribute == "*" || perm.Identifier == "*" { return map[string]bool{"*": true} } permMap[perm.Scope] = true } return permMap } func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) { if req.ParentFolder == "" { return false, nil } ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.checkInheritedPermissions") defer span.End() ctxLogger := s.logger.FromContext(ctx) tree, ok := s.getCachedFolderTree(ctx, req.Namespace) // Check cached tree is up to date if !ok || !s.isFolderInTree(tree, req.ParentFolder) { var err error tree, err = s.buildFolderTree(ctx, req.Namespace) if err != nil { ctxLogger.Error("could not build folder and dashboard tree", "error", err) return false, err } if !s.isFolderInTree(tree, req.ParentFolder) { // Not erroring here as the permission might exist but the folder wasn't synchronized yet // Once in mode 5 we can deny access here ctxLogger.Error("parent folder not found in folder tree", "folder", req.ParentFolder) } } if scopeMap["folders:uid:"+req.ParentFolder] { return true, nil } for n := range tree.Ancestors(req.ParentFolder) { if scopeMap["folders:uid:"+n.UID] { return true, nil } } return false, nil } // getCachedFolderTree returns the cached folder tree for the given namespace. func (s *Service) getCachedFolderTree(ctx context.Context, ns types.NamespaceInfo) (folderTree, bool) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getCachedFolderTree") defer span.End() key := folderCacheKey(ns.Value) return s.folderCache.Get(ctx, key) } // isFolderInTree checks if the given parent folder exists in the folder tree. func (s *Service) isFolderInTree(tree folderTree, folder string) bool { // Special case for general folder, which is technically not in the tree if folder == accesscontrol.GeneralFolderUID { return true } _, exists := tree.Index[folder] return exists } // buildFolderTree builds the folder tree for the given namespace and caches it. func (s *Service) buildFolderTree(ctx context.Context, ns types.NamespaceInfo) (folderTree, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.buildFolderTree") defer span.End() res, err, _ := s.sf.Do(ns.Value+"_buildFolderTree", func() (interface{}, error) { folders, err := s.folderStore.ListFolders(ctx, ns) if err != nil { return nil, fmt.Errorf("could not get folders: %w", err) } span.SetAttributes(attribute.Int("num_folders", len(folders))) tree := newFolderTree(folders) if len(tree.Nodes) != len(folders) { s.logger.FromContext(ctx).Warn("mismatched folder count when building tree", "namespace", ns.Value, "expected", len(folders), "got", len(tree.Nodes)) } s.folderCache.Set(ctx, folderCacheKey(ns.Value), tree) return tree, nil }) if err != nil { return folderTree{}, err } return res.(folderTree), nil } func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *listRequest) (*authzv1.ListResponse, error) { if scopeMap["*"] { return &authzv1.ListResponse{ All: true, Zookie: &authzv1.Zookie{Timestamp: time.Now().Unix()}, }, nil } ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.listPermission") defer span.End() ctxLogger := s.logger.FromContext(ctx) t, ok := s.mapper.Get(req.Group, req.Resource) if !ok { ctxLogger.Error("unsupported resource", "group", req.Group, "resource", req.Resource) return nil, status.Error(codes.NotFound, "unsupported resource") } var tree folderTree cacheHit := false if t.HasFolderSupport() { var err error if !req.Options.SkipCache { tree, cacheHit = s.getCachedFolderTree(ctx, req.Namespace) } if !cacheHit { tree, err = s.buildFolderTree(ctx, req.Namespace) if err != nil { ctxLogger.Error("could not build folder and dashboard tree", "error", err) return nil, err } } } var res *authzv1.ListResponse if strings.HasPrefix(req.Action, "folders:") || strings.HasPrefix(req.Action, "folders.permissions:") { res = buildFolderList(scopeMap, tree) } else { res = buildItemList(scopeMap, tree, t.Prefix()) } if cacheHit { res.Zookie = &authzv1.Zookie{Timestamp: time.Now().Add(-s.settings.CacheTTL).Unix()} } else { res.Zookie = &authzv1.Zookie{Timestamp: time.Now().Unix()} } span.SetAttributes(attribute.Int("num_folders", len(res.Folders)), attribute.Int("num_items", len(res.Items))) return res, nil } func buildFolderList(scopes map[string]bool, tree folderTree) *authzv1.ListResponse { itemSet := make(map[string]struct{}, len(scopes)) for scope := range scopes { identifier := strings.TrimPrefix(scope, "folders:uid:") if _, ok := itemSet[identifier]; ok { continue } itemSet[identifier] = struct{}{} for n := range tree.Children(identifier) { itemSet[n.UID] = struct{}{} } } itemList := make([]string, 0, len(itemSet)) for item := range itemSet { itemList = append(itemList, item) } return &authzv1.ListResponse{Items: itemList} } func buildItemList(scopes map[string]bool, tree folderTree, prefix string) *authzv1.ListResponse { folderSet := make(map[string]struct{}, len(scopes)) itemSet := make(map[string]struct{}, len(scopes)) for scope := range scopes { if identifier, ok := strings.CutPrefix(scope, "folders:uid:"); ok { if _, ok := folderSet[identifier]; ok { continue } folderSet[identifier] = struct{}{} for n := range tree.Children(identifier) { folderSet[n.UID] = struct{}{} } } else { identifier := strings.TrimPrefix(scope, prefix) itemSet[identifier] = struct{}{} } } folderList := make([]string, 0, len(folderSet)) for folder := range folderSet { folderList = append(folderList, folder) } itemList := make([]string, 0, len(itemSet)) for item := range itemSet { itemList = append(itemList, item) } return &authzv1.ListResponse{Folders: folderList, Items: itemList} }