From 2503b31f53e390c026eaa8c88acf5e3f26330740 Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 20 Dec 2024 13:48:20 +0000 Subject: [PATCH] AuthZ service: Implement listing (#98220) * listing implementation pt 1 * validate list request * register GRPC endpoint, pass the correct user UID and return folder identifiers not scopes * uncomment code that was only commented out for testing * fix tests * remove unneeded changes * remove unused import * Update pkg/services/authz/rbac/service.go Co-authored-by: Gabriel MABILLE * refactor to improve efficiency Co-authored-by: Gabriel MABILLE * use variable names when logging * adding tests for listing --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authz/rbac/models.go | 9 + pkg/services/authz/rbac/service.go | 228 ++++++++++++++++++------ pkg/services/authz/rbac/service_test.go | 206 +++++++++++++++++++-- 3 files changed, 380 insertions(+), 63 deletions(-) diff --git a/pkg/services/authz/rbac/models.go b/pkg/services/authz/rbac/models.go index 9fe24993bd6..07c301259e6 100644 --- a/pkg/services/authz/rbac/models.go +++ b/pkg/services/authz/rbac/models.go @@ -13,6 +13,15 @@ type CheckRequest struct { ParentFolder string } +type ListRequest struct { + Namespace claims.NamespaceInfo + UserUID string + Group string + Resource string + Verb string + Action string +} + type FolderNode struct { uid string parentUID *string diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index ec0c696f822..085219fdfcd 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "time" authzv1 "github.com/grafana/authlib/authz/proto/v1" @@ -78,14 +79,14 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv deny := &authzv1.CheckResponse{Allowed: false} - checkReq, err := s.validateRequest(ctx, req) + checkReq, err := s.validateCheckRequest(ctx, req) if err != nil { ctxLogger.Error("invalid request", "error", err) return deny, err } ctx = request.WithNamespace(ctx, req.GetNamespace()) - permissions, err := s.getUserPermissions(ctx, checkReq) + permissions, err := s.getUserPermissions(ctx, checkReq.Namespace, checkReq.UserUID, checkReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return deny, err @@ -99,47 +100,41 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv return &authzv1.CheckResponse{Allowed: allowed}, nil } -func (s *Service) validateRequest(ctx context.Context, req *authzv1.CheckRequest) (*CheckRequest, error) { +func (s *Service) List(ctx context.Context, req *authzextv1.ListRequest) (*authzextv1.ListResponse, error) { + ctx, span := s.tracer.Start(ctx, "authz_direct_db.List") + defer span.End() ctxLogger := s.logger.FromContext(ctx) - if req.GetNamespace() == "" { - return nil, status.Error(codes.InvalidArgument, "namespace is required") - } - authInfo, has := claims.From(ctx) - if !has { - return nil, status.Error(codes.Internal, "could not get auth info from context") - } - if !claims.NamespaceMatches(authInfo.GetNamespace(), req.GetNamespace()) { - return nil, status.Error(codes.PermissionDenied, "namespace does not match") - } - - ns, err := claims.ParseNamespace(req.GetNamespace()) + listReq, err := s.validateListRequest(ctx, req) if err != nil { - ctxLogger.Error("could not parse namespace", "namespace", req.GetNamespace(), "error", err) + ctxLogger.Error("invalid request", "error", err) + return &authzextv1.ListResponse{}, err + } + ctx = request.WithNamespace(ctx, req.GetNamespace()) + + permissions, err := s.getUserPermissions(ctx, listReq.Namespace, listReq.UserUID, listReq.Action) + if err != nil { + ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return nil, err } - if req.GetSubject() == "" { - return nil, status.Error(codes.InvalidArgument, "subject is required") - } - user := req.GetSubject() - identityType, userUID, err := claims.ParseTypeID(user) + return s.listPermission(ctx, permissions, listReq) +} + +func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*CheckRequest, error) { + ns, err := validateNamespace(ctx, req.GetNamespace()) if err != nil { - ctxLogger.Error("could not parse subject", "subject", user, "error", err) return nil, err } - // Permission check currently only checks user and service account permissions, so might return a false negative for other types - if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount) { - ctxLogger.Warn("unsupported identity type", "type", identityType) + + userUID, err := s.validateSubject(ctx, req.GetSubject()) + if err != nil { + return nil, err } - if req.GetGroup() == "" || req.GetResource() == "" || req.GetVerb() == "" { - return nil, status.Error(codes.InvalidArgument, "group, resource and verb are required") - } - action, ok := s.actionMapper.Action(req.GetGroup(), req.GetResource(), req.GetVerb()) - if !ok { - ctxLogger.Error("could not find associated rbac action", "group", req.GetGroup(), "resource", req.GetResource(), "verb", req.GetVerb()) - return nil, status.Error(codes.NotFound, "could not find associated rbac action") + action, err := s.validateAction(ctx, req.GetGroup(), req.GetResource(), req.GetVerb()) + if err != nil { + return nil, err } checkReq := &CheckRequest{ @@ -155,36 +150,112 @@ func (s *Service) validateRequest(ctx context.Context, req *authzv1.CheckRequest return checkReq, nil } -func (s *Service) getUserPermissions(ctx context.Context, req *CheckRequest) (map[string]bool, error) { - userIdentifiers, err := s.GetUserIdentifiers(ctx, req) +func (s *Service) validateListRequest(ctx context.Context, req *authzextv1.ListRequest) (*ListRequest, error) { + ns, err := validateNamespace(ctx, req.GetNamespace()) if err != nil { return nil, err } - userPermKey := userPermCacheKey(req.Namespace.Value, userIdentifiers.UID, req.Action) + userUID, 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, + Action: action, + Group: req.GetGroup(), + Resource: req.GetResource(), + Verb: req.GetVerb(), + } + return listReq, nil +} + +func validateNamespace(ctx context.Context, nameSpace string) (claims.NamespaceInfo, error) { + if nameSpace == "" { + return claims.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required") + } + authInfo, has := claims.From(ctx) + if !has { + return claims.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context") + } + if !claims.NamespaceMatches(authInfo.GetNamespace(), nameSpace) { + return claims.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match") + } + + ns, err := claims.ParseNamespace(nameSpace) + if err != nil { + return claims.NamespaceInfo{}, err + } + return ns, nil +} + +func (s *Service) validateSubject(ctx context.Context, subject string) (string, error) { + if subject == "" { + return "", status.Error(codes.InvalidArgument, "subject is required") + } + + ctxLogger := s.logger.FromContext(ctx) + identityType, userUID, err := claims.ParseTypeID(subject) + if err != nil { + return "", err + } + // Permission check currently only checks user and service account permissions, so might return a false negative for other types + if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount) { + ctxLogger.Warn("unsupported identity type", "type", identityType) + } + return userUID, nil +} + +func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) { + ctxLogger := s.logger.FromContext(ctx) + if group == "" || resource == "" || verb == "" { + return "", status.Error(codes.InvalidArgument, "group, resource and verb are required") + } + action, ok := s.actionMapper.Action(group, resource, verb) + if !ok { + ctxLogger.Error("could not find associated rbac action", "group", group, "resource", resource, "verb", verb) + return "", status.Error(codes.NotFound, "could not find associated rbac action") + } + return action, nil +} + +func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, userID, action string) (map[string]bool, error) { + 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(userPermKey); ok { return cached.(map[string]bool), nil } - basicRoles, err := s.getUserBasicRole(ctx, req, userIdentifiers) + basicRoles, err := s.getUserBasicRole(ctx, ns, userIdentifiers) if err != nil { return nil, err } - teamIDs, err := s.getUserTeams(ctx, req, userIdentifiers) + teamIDs, err := s.getUserTeams(ctx, ns, userIdentifiers) if err != nil { return nil, err } userPermQuery := store.PermissionsQuery{ UserID: userIdentifiers.ID, - Action: req.Action, + Action: action, TeamIDs: teamIDs, Role: basicRoles.Role, IsServerAdmin: basicRoles.IsAdmin, } - permissions, err := s.store.GetUserPermissions(ctx, req.Namespace, userPermQuery) + permissions, err := s.store.GetUserPermissions(ctx, ns, userPermQuery) if err != nil { return nil, err } @@ -193,23 +264,23 @@ func (s *Service) getUserPermissions(ctx context.Context, req *CheckRequest) (ma return scopeMap, nil } -func (s *Service) GetUserIdentifiers(ctx context.Context, req *CheckRequest) (*store.UserIdentifiers, error) { - uidCacheKey := userIdentifierCacheKey(req.Namespace.Value, req.UserUID) +func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { + uidCacheKey := userIdentifierCacheKey(ns.Value, userUID) if cached, ok := s.idCache.Get(uidCacheKey); ok { return cached.(*store.UserIdentifiers), nil } - idCacheKey := userIdentifierCacheKeyById(req.Namespace.Value, req.UserUID) + idCacheKey := userIdentifierCacheKeyById(ns.Value, userUID) if cached, ok := s.idCache.Get(idCacheKey); ok { return cached.(*store.UserIdentifiers), nil } var userIDQuery store.UserIdentifierQuery // Assume that numeric UID is user ID - if userID, err := strconv.Atoi(req.UserUID); err == nil { + if userID, err := strconv.Atoi(userUID); err == nil { userIDQuery = store.UserIdentifierQuery{UserID: int64(userID)} } else { - userIDQuery = store.UserIdentifierQuery{UserUID: req.UserUID} + userIDQuery = store.UserIdentifierQuery{UserUID: userUID} } userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery) if err != nil { @@ -222,9 +293,9 @@ func (s *Service) GetUserIdentifiers(ctx context.Context, req *CheckRequest) (*s return userIdentifiers, nil } -func (s *Service) getUserTeams(ctx context.Context, req *CheckRequest, userIdentifiers *store.UserIdentifiers) ([]int64, error) { +func (s *Service) getUserTeams(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) { teamIDs := make([]int64, 0, 50) - teamsCacheKey := userTeamCacheKey(req.Namespace.Value, userIdentifiers.UID) + teamsCacheKey := userTeamCacheKey(ns.Value, userIdentifiers.UID) if cached, ok := s.teamCache.Get(teamsCacheKey); ok { return cached.([]int64), nil } @@ -235,7 +306,7 @@ func (s *Service) getUserTeams(ctx context.Context, req *CheckRequest, userIdent } for { - teams, err := s.identityStore.ListUserTeams(ctx, req.Namespace, teamQuery) + teams, err := s.identityStore.ListUserTeams(ctx, ns, teamQuery) if err != nil { return nil, fmt.Errorf("could not get user teams: %w", err) } @@ -252,13 +323,13 @@ func (s *Service) getUserTeams(ctx context.Context, req *CheckRequest, userIdent return teamIDs, nil } -func (s *Service) getUserBasicRole(ctx context.Context, req *CheckRequest, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) { - basicRoleKey := userBasicRoleCacheKey(req.Namespace.Value, userIdentifiers.UID) +func (s *Service) getUserBasicRole(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) { + basicRoleKey := userBasicRoleCacheKey(ns.Value, userIdentifiers.UID) if cached, ok := s.basicRoleCache.Get(basicRoleKey); ok { return cached.(store.BasicRole), nil } - basicRole, err := s.store.GetBasicRoles(ctx, req.Namespace, store.BasicRoleQuery{UserID: userIdentifiers.ID}) + 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) } @@ -386,3 +457,60 @@ func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) return res.(map[string]FolderNode), nil } + +func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *ListRequest) (*authzextv1.ListResponse, error) { + if scopeMap["*"] { + return &authzextv1.ListResponse{All: true}, nil + } + + ctxLogger := s.logger.FromContext(ctx) + + folderMap, err := s.buildFolderTree(ctx, req.Namespace) + if err != nil { + ctxLogger.Error("could not build folder and dashboard tree", "error", err) + return nil, err + } + + folderSet := make(map[string]struct{}, len(scopeMap)) + dashSet := make(map[string]struct{}, len(scopeMap)) + for scope := range scopeMap { + if strings.HasPrefix(scope, "folders:uid:") { + identifier := scope[len("folders:uid:"):] + if _, ok := folderSet[identifier]; ok { + continue + } + folderSet[identifier] = struct{}{} + getChildren(folderMap, identifier, folderSet) + } else if strings.HasPrefix(scope, "dashboards:uid:") { + identifier := scope[len("dashboards:uid:"):] + dashSet[identifier] = struct{}{} + } + } + + folderList := make([]string, 0, len(folderSet)) + for folder := range folderSet { + folderList = append(folderList, folder) + } + + dashList := make([]string, 0, len(dashSet)) + for dash := range dashSet { + dashList = append(dashList, dash) + } + + return &authzextv1.ListResponse{Folders: folderList, Items: dashList}, nil +} + +func getChildren(folderMap map[string]FolderNode, folderUID string, folderSet map[string]struct{}) { + folder, has := folderMap[folderUID] + if !has { + return + } + for _, child := range folder.childrenUIDs { + // We have already processed all the children of this folder + if _, ok := folderSet[child]; ok { + return + } + folderSet[child] = struct{}{} + getChildren(folderMap, child, folderSet) + } +} diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index e1b1487fac4..eb4ba1895a6 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -156,6 +156,7 @@ func TestService_checkPermission(t *testing.T) { }) } } + func TestService_getUserTeams(t *testing.T) { type testCase struct { name string @@ -192,14 +193,14 @@ func TestService_getUserTeams(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - req := &CheckRequest{Namespace: claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12}} + ns := claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} userIdentifiers := &store.UserIdentifiers{UID: "test-uid"} identityStore := &fakeIdentityStore{teams: tc.teams, err: tc.expectedError} cacheService := localcache.New(shortCacheTTL, shortCleanupInterval) if tc.cacheHit { - cacheService.Set(userTeamCacheKey(req.Namespace.Value, userIdentifiers.UID), tc.expectedTeams, 0) + cacheService.Set(userTeamCacheKey(ns.Value, userIdentifiers.UID), tc.expectedTeams, 0) } s := &Service{ @@ -208,7 +209,7 @@ func TestService_getUserTeams(t *testing.T) { logger: log.New("test"), } - teams, err := s.getUserTeams(ctx, req, userIdentifiers) + teams, err := s.getUserTeams(ctx, ns, userIdentifiers) if tc.expectedError { require.Error(t, err) return @@ -273,14 +274,14 @@ func TestService_getUserBasicRole(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - req := &CheckRequest{Namespace: claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12}} + ns := claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} userIdentifiers := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{basicRole: &tc.basicRole, err: tc.expectedError} cacheService := localcache.New(shortCacheTTL, shortCleanupInterval) if tc.cacheHit { - cacheService.Set(userBasicRoleCacheKey(req.Namespace.Value, userIdentifiers.UID), tc.expectedRole, 0) + cacheService.Set(userBasicRoleCacheKey(ns.Value, userIdentifiers.UID), tc.expectedRole, 0) } s := &Service{ @@ -289,7 +290,7 @@ func TestService_getUserBasicRole(t *testing.T) { logger: log.New("test"), } - role, err := s.getUserBasicRole(ctx, req, userIdentifiers) + role, err := s.getUserBasicRole(ctx, ns, userIdentifiers) if tc.expectedError { require.Error(t, err) return @@ -343,15 +344,12 @@ func TestService_getUserPermissions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() userID := &store.UserIdentifiers{UID: "test-uid", ID: 112} - req := &CheckRequest{ - Namespace: claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12}, - UserUID: userID.UID, - Action: "dashboards:read", - } + ns := claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} + action := "dashboards:read" cacheService := localcache.New(shortCacheTTL, shortCleanupInterval) if tc.cacheHit { - cacheService.Set(userPermCacheKey(req.Namespace.Value, userID.UID, req.Action), tc.expectedPerms, 0) + cacheService.Set(userPermCacheKey(ns.Value, userID.UID, action), tc.expectedPerms, 0) } store := &fakeStore{ @@ -372,7 +370,7 @@ func TestService_getUserPermissions(t *testing.T) { teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), } - perms, err := s.getUserPermissions(ctx, req) + perms, err := s.getUserPermissions(ctx, ns, userID.UID, action) require.NoError(t, err) require.Len(t, perms, len(tc.expectedPerms)) for _, perm := range tc.permissions { @@ -471,6 +469,188 @@ func TestService_buildFolderTree(t *testing.T) { } } +func TestService_listPermission(t *testing.T) { + type testCase struct { + name string + permissions []accesscontrol.Permission + folderTree map[string]FolderNode + list ListRequest + expectedDashboards []string + expectedFolders []string + expectedAll bool + } + + testCases := []testCase{ + { + name: "should return wildcard if user has a wildcard permission", + permissions: []accesscontrol.Permission{ + { + Action: "dashboards:read", + Scope: "*", + Kind: "*", + }, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + expectedAll: true, + }, + { + name: "should return dashboards and folders that user has direct access to", + permissions: []accesscontrol.Permission{ + { + Action: "dashboards:read", + Scope: "dashboards:uid:some_dashboard", + Kind: "dashboards", + Attribute: "uid", + Identifier: "some_dashboard", + }, + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_1", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_1", + }, + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_2", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_2", + }, + }, + folderTree: map[string]FolderNode{ + "some_folder_1": {uid: "some_folder_1"}, + "some_folder_2": {uid: "some_folder_2"}, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + expectedDashboards: []string{"some_dashboard"}, + expectedFolders: []string{"some_folder_1", "some_folder_2"}, + }, + { + name: "should return folders that user has inherited access to", + permissions: []accesscontrol.Permission{ + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_parent", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_1", + }, + }, + folderTree: map[string]FolderNode{ + "some_folder_parent": {uid: "some_folder_parent", childrenUIDs: []string{"some_folder_child"}}, + "some_folder_child": {uid: "some_folder_child", parentUID: strPtr("some_folder_parent"), childrenUIDs: []string{"some_folder_subchild1", "some_folder_subchild2"}}, + "some_folder_subchild1": {uid: "some_folder_subchild1", parentUID: strPtr("some_folder_child")}, + "some_folder_subchild2": {uid: "some_folder_subchild2", parentUID: strPtr("some_folder_child"), childrenUIDs: []string{"some_folder_subsubchild"}}, + "some_folder_subsubchild": {uid: "some_folder_subsubchild", parentUID: strPtr("some_folder_subchild2")}, + "some_folder_1": {uid: "some_folder_1", parentUID: strPtr("some_other_folder")}, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + expectedFolders: []string{"some_folder_parent", "some_folder_child", "some_folder_subchild1", "some_folder_subchild2", "some_folder_subsubchild"}, + }, + { + name: "should return folders that user has inherited access to as well as dashboards that user has direct access to", + permissions: []accesscontrol.Permission{ + { + Action: "dashboards:read", + Scope: "dashboards:uid:some_dashboard", + Kind: "dashboards", + Attribute: "uid", + Identifier: "some_dashboard", + }, + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_parent", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_parent", + }, + }, + folderTree: map[string]FolderNode{ + "some_folder_parent": {uid: "some_folder_parent", childrenUIDs: []string{"some_folder_child"}}, + "some_folder_child": {uid: "some_folder_child", parentUID: strPtr("some_folder_parent")}, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + expectedDashboards: []string{"some_dashboard"}, + expectedFolders: []string{"some_folder_parent", "some_folder_child"}, + }, + { + name: "should deduplicate folders that user has inherited as well as direct access to", + permissions: []accesscontrol.Permission{ + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_child", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_child", + }, + { + Action: "dashboards:read", + Scope: "folders:uid:some_folder_parent", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_parent", + }, + }, + folderTree: map[string]FolderNode{ + "some_folder_parent": {uid: "some_folder_parent", childrenUIDs: []string{"some_folder_child"}}, + "some_folder_child": {uid: "some_folder_child", parentUID: strPtr("some_folder_parent"), childrenUIDs: []string{"some_folder_subchild"}}, + "some_folder_subchild": {uid: "some_folder_subchild", parentUID: strPtr("some_folder_child")}, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + expectedFolders: []string{"some_folder_parent", "some_folder_child", "some_folder_subchild"}, + }, + { + name: "return no dashboards and folders if the user doesn't have access to any resources", + permissions: []accesscontrol.Permission{}, + folderTree: map[string]FolderNode{ + "some_folder_1": {uid: "some_folder_1"}, + }, + list: ListRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + folderCache := localcache.New(shortCacheTTL, shortCleanupInterval) + if tc.folderTree != nil { + folderCache.Set(folderCacheKey("default"), tc.folderTree, 0) + } + s := &Service{logger: log.New("test"), actionMapper: mappers.NewK8sRbacMapper(), folderCache: folderCache} + tc.list.Namespace = claims.NamespaceInfo{Value: "default", OrgID: 1} + got, err := s.listPermission(context.Background(), getScopeMap(tc.permissions), &tc.list) + require.NoError(t, err) + assert.Equal(t, tc.expectedAll, got.All) + assert.ElementsMatch(t, tc.expectedDashboards, got.Items) + assert.ElementsMatch(t, tc.expectedFolders, got.Folders) + }) + } +} + func strPtr(s string) *string { return &s }