mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
833f5d5d22
commit
2503b31f53
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user