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 <gamab@users.noreply.github.com>

* refactor to improve efficiency

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* use variable names when logging

* adding tests for listing

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Ieva 2024-12-20 13:48:20 +00:00 committed by GitHub
parent 833f5d5d22
commit 2503b31f53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 380 additions and 63 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}