656 lines
20 KiB
Go
656 lines
20 KiB
Go
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/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
|
|
shortCleanupInterval = 2 * time.Minute
|
|
longCacheTTL = 2 * time.Minute
|
|
longCleanupInterval = 4 * time.Minute
|
|
)
|
|
|
|
type Service struct {
|
|
authzv1.UnimplementedAuthzServiceServer
|
|
|
|
store store.Store
|
|
folderStore store.FolderStore
|
|
permissionStore store.PermissionStore
|
|
identityStore legacy.LegacyIdentityStore
|
|
|
|
mapper mapper
|
|
|
|
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]
|
|
teamCache *cacheWrap[[]int64]
|
|
basicRoleCache *cacheWrap[store.BasicRole]
|
|
folderCache *cacheWrap[folderTree]
|
|
}
|
|
|
|
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,
|
|
) *Service {
|
|
return &Service{
|
|
store: store.NewStore(sql, tracer),
|
|
folderStore: folderStore,
|
|
permissionStore: permissionStore,
|
|
identityStore: identityStore,
|
|
logger: logger,
|
|
tracer: tracer,
|
|
metrics: newMetrics(reg),
|
|
mapper: newMapper(),
|
|
idCache: newCacheWrap[store.UserIdentifiers](cache, logger, longCacheTTL),
|
|
permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL),
|
|
teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL),
|
|
basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, shortCacheTTL),
|
|
folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL),
|
|
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)
|
|
|
|
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())
|
|
|
|
permissions, err := s.getIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action)
|
|
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
|
|
}
|
|
|
|
s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
|
|
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)
|
|
|
|
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())
|
|
|
|
permissions, err := s.getIdentityPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action)
|
|
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()
|
|
return resp, err
|
|
}
|
|
|
|
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, 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,
|
|
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, err := s.validateAction(ctx, req.GetGroup(), req.GetResource(), req.GetVerb())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
listReq := &ListRequest{
|
|
Namespace: ns,
|
|
UserUID: userUID,
|
|
IdentityType: idType,
|
|
Action: action,
|
|
Group: req.GetGroup(),
|
|
Resource: req.GetResource(),
|
|
Verb: req.GetVerb(),
|
|
}
|
|
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
|
|
}
|
|
|
|
func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) {
|
|
ctxLogger := s.logger.FromContext(ctx)
|
|
|
|
t, ok := s.mapper.translation(group, resource)
|
|
if !ok {
|
|
ctxLogger.Error("unsupport resource", "group", group, "resource", resource)
|
|
return "", status.Error(codes.NotFound, "unsupported resource")
|
|
}
|
|
|
|
action, ok := t.action(verb)
|
|
if !ok {
|
|
ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb)
|
|
return "", status.Error(codes.NotFound, "unsupported verb")
|
|
}
|
|
|
|
return action, nil
|
|
}
|
|
|
|
func (s *Service) getIdentityPermissions(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.getIdentityPermissions")
|
|
defer span.End()
|
|
|
|
// When checking folder creation permissions, also check edit and admin action sets for folder, as the scoped folder create actions aren't stored in the DB separately
|
|
var actionSets []string
|
|
if action == "folders:create" {
|
|
actionSets = append(actionSets, "folders:edit", "folders:admin")
|
|
}
|
|
|
|
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) 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)
|
|
if cached, ok := s.permCache.Get(ctx, userPermKey); ok {
|
|
s.metrics.permissionCacheUsage.WithLabelValues("true", action).Inc()
|
|
return cached, nil
|
|
}
|
|
s.metrics.permissionCacheUsage.WithLabelValues("false", action).Inc()
|
|
|
|
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 := getScopeMap(permissions)
|
|
|
|
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)
|
|
if cached, ok := s.permCache.Get(ctx, anonPermKey); ok {
|
|
return cached, nil
|
|
}
|
|
res, err, _ := s.sf.Do(anonPermKey+"_getAnonymousPermissions", func() (interface{}, error) {
|
|
permissions, err := s.permissionStore.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, ActionSets: actionSets, Role: "Viewer"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scopeMap := 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.teamCache.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.teamCache.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 == "" {
|
|
return len(scopeMap) > 0, nil
|
|
}
|
|
|
|
// Wildcard grant, no further checks needed
|
|
if scopeMap["*"] {
|
|
return true, nil
|
|
}
|
|
|
|
t, ok := s.mapper.translation(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 scopeMap[t.scope(req.Name)] {
|
|
return true, nil
|
|
}
|
|
|
|
if !t.folderSupport {
|
|
return false, nil
|
|
}
|
|
|
|
return s.checkInheritedPermissions(ctx, scopeMap, req)
|
|
}
|
|
|
|
func getScopeMap(permissions []accesscontrol.Permission) map[string]bool {
|
|
permMap := make(map[string]bool, len(permissions))
|
|
for _, perm := range permissions {
|
|
// 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, err := s.buildFolderTree(ctx, req.Namespace)
|
|
if err != nil {
|
|
ctxLogger.Error("could not build folder and dashboard tree", "error", err)
|
|
return false, err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
|
|
key := folderCacheKey(ns.Value)
|
|
if cached, ok := s.folderCache.Get(ctx, key); ok {
|
|
return cached, nil
|
|
}
|
|
|
|
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)
|
|
s.folderCache.Set(ctx, key, 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}, nil
|
|
}
|
|
|
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.listPermission")
|
|
defer span.End()
|
|
ctxLogger := s.logger.FromContext(ctx)
|
|
|
|
t, ok := s.mapper.translation(req.Group, req.Resource)
|
|
if !ok {
|
|
ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource)
|
|
return nil, status.Error(codes.NotFound, "unsupported resource")
|
|
}
|
|
|
|
var tree folderTree
|
|
if t.folderSupport {
|
|
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 nil, err
|
|
}
|
|
}
|
|
|
|
var res *authzv1.ListResponse
|
|
if strings.HasPrefix(req.Action, "folders:") {
|
|
res = buildFolderList(scopeMap, tree)
|
|
} else {
|
|
res = buildItemList(scopeMap, tree, t.prefix())
|
|
}
|
|
|
|
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}
|
|
}
|