package rbac import ( "context" "fmt" "slices" "testing" "time" "github.com/go-jose/go-jose/v4/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/singleflight" "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/authlib/authn" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/cache" "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/team" ) func TestService_checkPermission(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission check checkRequest folders []store.Folder expected bool } testCases := []testCase{ { name: "should return true if user has permission", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "dashboards:uid:some_dashboard", Kind: "dashboards", Attribute: "uid", Identifier: "some_dashboard", }, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: true, }, { name: "should return false if user has permission on a different resource", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "dashboards:uid:another_dashboard", Kind: "dashboards", Attribute: "uid", Identifier: "another_dashboard", }, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: false, }, { name: "should return true if user has wildcard permission on identifier", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "dashboards:uid:*", Kind: "dashboards", Attribute: "uid", Identifier: "*", }, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: true, }, { name: "should return true if user has wildcard permission on attribute", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "dashboards:*", Kind: "dashboards", Attribute: "*", }, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: true, }, { name: "should return true if user has wildcard permission on kind", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "*", Kind: "*", }, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: true, }, { name: "should check general folder scope for root level resource creation", permissions: []accesscontrol.Permission{ { Action: "dashboards:create", Scope: "folders:uid:general", Kind: "folders", Attribute: "uid", Identifier: "general", }, }, check: checkRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: utils.VerbCreate, }, expected: true, }, { name: "should fail if user doesn't have general folder scope for root level resource creation", permissions: []accesscontrol.Permission{ { Action: "dashboards:create", }, }, check: checkRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: utils.VerbCreate, }, expected: false, }, { name: "should return false if user has no permissions on resource", permissions: []accesscontrol.Permission{}, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", }, expected: false, }, { name: "should return true if user has permissions on folder", permissions: []accesscontrol.Permission{ { Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent", }, }, folders: []store.Folder{ {UID: "parent"}, {UID: "child", ParentUID: strPtr("parent")}, }, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "some_dashboard", ParentFolder: "child", }, expected: true, }, { name: "should allow creating a nested resource", permissions: []accesscontrol.Permission{ { Action: "dashboards:create", Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent", }, }, folders: []store.Folder{{UID: "parent"}}, check: checkRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "", ParentFolder: "parent", Verb: utils.VerbCreate, }, expected: true, }, { name: "should deny creating a nested resource", permissions: []accesscontrol.Permission{ { Action: "dashboards:create", Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent", }, }, folders: []store.Folder{{UID: "parent"}, {UID: "other_parent"}}, check: checkRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "", ParentFolder: "other_parent", Verb: utils.VerbCreate, }, expected: false, }, { name: "should allow if it's an any check", permissions: []accesscontrol.Permission{ { Action: "dashboards:read", Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent", }, }, folders: []store.Folder{{UID: "parent"}}, check: checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "", ParentFolder: "", Verb: utils.VerbList, }, expected: true, }, { name: "should allow querying a datasource", permissions: []accesscontrol.Permission{ { Action: "datasources:query", Scope: "datasources:uid:ds1", Kind: "datasources", Attribute: "uid", Identifier: "ds1", }, }, check: checkRequest{ Action: "datasources:query", Group: "query.grafana.app", Resource: "query", Name: "ds1", Verb: utils.VerbCreate, }, expected: true, }, { name: "should deny querying a datasource without correct permission", permissions: []accesscontrol.Permission{ { Action: "datasources:query", Scope: "datasources:uid:ds2", Kind: "datasources", Attribute: "uid", Identifier: "ds2", }, }, check: checkRequest{ Action: "datasources:query", Group: "query.grafana.app", Resource: "query", Name: "ds1", Verb: utils.VerbCreate, }, expected: false, }, { name: "should allow creating a team (no scope needed)", permissions: []accesscontrol.Permission{ {Action: "teams:create"}, }, check: checkRequest{ Action: "teams:create", Group: "iam.grafana.app", Resource: "teams", Verb: utils.VerbCreate, }, expected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() s.folderCache.Set(context.Background(), folderCacheKey("default"), newFolderTree(tc.folders)) tc.check.Namespace = types.NamespaceInfo{Value: "default", OrgID: 1} got, err := s.checkPermission(context.Background(), s.getScopeMap(tc.permissions), &tc.check) require.NoError(t, err) assert.Equal(t, tc.expected, got) }) } } func TestService_mapping(t *testing.T) { type testCase struct { name string input *authzv1.CheckRequest output *checkRequest err string } ns := "default" testUserA := &identity.StaticRequester{ Type: types.TypeUser, Login: "test", UserID: 123, UserUID: "u123", OrgRole: identity.RoleAdmin, IsGrafanaAdmin: true, // can do anything Namespace: ns, OrgID: 1, } ctx := types.WithAuthInfo(request.WithNamespace(context.Background(), ns), testUserA) testCases := []testCase{ { name: "should return true if user has permission", input: &authzv1.CheckRequest{ Group: "folder.grafana.app", Resource: "folders", Name: "aaa", Verb: utils.VerbCreate, Folder: "folder", }, output: &checkRequest{ Action: "folders:create", ActionSets: []string{"folders:edit", "folders:admin"}, Group: "folder.grafana.app", Resource: "folders", Name: "aaa", Verb: "create", ParentFolder: "folder", Namespace: types.NamespaceInfo{ Value: ns, OrgID: 1, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() tc.input.Namespace = ns tc.input.Subject = testUserA.GetUID() // the subject string got, err := s.validateCheckRequest(ctx, tc.input) if tc.err != "" { require.Error(t, err) require.ErrorContains(t, err, tc.err) return } require.NoError(t, err) require.NotNil(t, got) tc.output.IdentityType = types.TypeUser tc.output.UserUID = testUserA.GetIdentifier() require.Equal(t, tc.output, got) }) } } func TestService_checkPermission_folderCacheMissRecovery(t *testing.T) { s := setupService() ctx := context.Background() // User has root folder access userPermissions := map[string]bool{ "folders:uid:root": true, } // Populate store with folders folderStore := &fakeStore{ folders: []store.Folder{{UID: "root"}, {UID: "sub", ParentUID: strPtr("root")}}, disableNsCheck: true, } s.folderStore = folderStore // Sub folder is missing from the cache s.folderCache.Set(ctx, folderCacheKey("default"), newFolderTree([]store.Folder{{UID: "root"}})) // Perform check on sub folder check := checkRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", ParentFolder: "sub", Namespace: types.NamespaceInfo{Value: "default", OrgID: 1}, } got, err := s.checkPermission(ctx, userPermissions, &check) require.NoError(t, err) assert.True(t, got) // Check that folder store was queried despite the initial cache hit assert.Equal(t, 1, folderStore.calls) } func TestService_listPermission_skipCache(t *testing.T) { s := setupService() ctx := context.Background() // User has root folder access userPermissions := map[string]bool{ "folders:uid:root": true, } // Populate store with folders folderStore := &fakeStore{ folders: []store.Folder{{UID: "root"}, {UID: "sub", ParentUID: strPtr("root")}}, disableNsCheck: true, } s.folderStore = folderStore // Sub folder is missing from the cache s.folderCache.Set(ctx, folderCacheKey("default"), newFolderTree([]store.Folder{{UID: "root"}})) // Perform list listReq := listRequest{ Action: "folders:read", Group: "folder.grafana.app", Resource: "folders", Namespace: types.NamespaceInfo{Value: "default", OrgID: 1}, Options: &ListRequestOptions{SkipCache: true}, } res, err := s.listPermission(ctx, userPermissions, &listReq) require.NoError(t, err) // Check that all folders are in returned list assert.Len(t, res.GetItems(), 2) // Check that folder store was queried assert.Equal(t, 1, folderStore.calls) } func TestService_getUserTeams(t *testing.T) { type testCase struct { name string teams []int64 cacheHit bool expectedTeams []int64 expectedError bool } testCases := []testCase{ { name: "should return teams from cache if available", teams: []int64{1, 2}, cacheHit: true, expectedTeams: []int64{1, 2}, expectedError: false, }, { name: "should return teams from identity store if not in cache", teams: []int64{3, 4}, cacheHit: false, expectedTeams: []int64{3, 4}, expectedError: false, }, { name: "should return error if identity store fails", teams: []int64{}, cacheHit: false, expectedTeams: nil, expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := setupService() ns := types.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} userIdentifiers := &store.UserIdentifiers{UID: "test-uid"} identityStore := &fakeIdentityStore{userTeams: tc.teams, err: tc.expectedError, disableNsCheck: true} s.identityStore = identityStore if tc.cacheHit { s.userTeamCache.Set(ctx, userTeamCacheKey(ns.Value, userIdentifiers.UID), tc.expectedTeams) } teams, err := s.getUserTeams(ctx, ns, userIdentifiers) if tc.expectedError { require.Error(t, err) return } require.NoError(t, err) require.ElementsMatch(t, tc.expectedTeams, teams) if tc.cacheHit { require.Zero(t, identityStore.calls) } else { require.Equal(t, 1, identityStore.calls) } }) } } func TestService_getUserBasicRole(t *testing.T) { type testCase struct { name string basicRole store.BasicRole cacheHit bool expectedRole store.BasicRole expectedError bool } testCases := []testCase{ { name: "should return basic role from cache if available", basicRole: store.BasicRole{ Role: "viewer", IsAdmin: false, }, cacheHit: true, expectedRole: store.BasicRole{ Role: "viewer", IsAdmin: false, }, expectedError: false, }, { name: "should return basic role from store if not in cache", basicRole: store.BasicRole{ Role: "editor", IsAdmin: false, }, cacheHit: false, expectedRole: store.BasicRole{ Role: "editor", IsAdmin: false, }, expectedError: false, }, { name: "should return error if store fails", basicRole: store.BasicRole{}, cacheHit: false, expectedRole: store.BasicRole{}, expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := setupService() ns := types.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} userIdentifiers := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{basicRole: &tc.basicRole, err: tc.expectedError, disableNsCheck: true} s.store = store s.permissionStore = store if tc.cacheHit { s.basicRoleCache.Set(ctx, userBasicRoleCacheKey(ns.Value, userIdentifiers.UID), tc.expectedRole) } role, err := s.getUserBasicRole(ctx, ns, userIdentifiers) if tc.expectedError { require.Error(t, err) return } require.NoError(t, err) require.Equal(t, tc.expectedRole, role) if tc.cacheHit { require.Zero(t, store.calls) } else { require.Equal(t, 1, store.calls) } }) } } func TestService_getUserPermissions(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission action string cacheHit bool expectedPerms map[string]bool } testCases := []testCase{ { name: "should return permissions from store if not in cache", permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:some_dashboard"}, }, action: "dashboards:read", cacheHit: false, expectedPerms: map[string]bool{"dashboards:uid:some_dashboard": true}, }, { name: "should return error if store fails", permissions: nil, action: "dashboards:read", cacheHit: false, expectedPerms: map[string]bool{}, }, { name: "should return uid based permissions", permissions: []accesscontrol.Permission{ {Action: "teams:read", Scope: "teams:id:1"}, }, action: "teams:read", cacheHit: false, expectedPerms: map[string]bool{"teams:uid:t1": true}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := setupService() ns := types.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} userID := &store.UserIdentifiers{UID: "test-uid", ID: 112} if tc.cacheHit { s.permCache.Set(ctx, userPermCacheKey(ns.Value, userID.UID, tc.action), tc.expectedPerms) } store := &fakeStore{ userID: userID, basicRole: &store.BasicRole{Role: "viewer", IsAdmin: false}, userPermissions: tc.permissions, disableNsCheck: true, } s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{ userTeams: []int64{1, 2}, teams: []team.Team{ {ID: 1, UID: "t1", OrgID: 1}, {ID: 2, UID: "t2", OrgID: 1}, }, disableNsCheck: true, } perms, err := s.getIdentityPermissions(ctx, ns, types.TypeUser, userID.UID, tc.action, nil) require.NoError(t, err) require.Len(t, perms, len(tc.expectedPerms)) for scope := range perms { _, ok := tc.expectedPerms[scope] require.True(t, ok) } if tc.cacheHit { require.Equal(t, 1, store.calls) // only get user id } else { require.Equal(t, 3, store.calls) // get user id, basic role, and permissions } }) } } func TestService_listPermission(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission folders []store.Folder list listRequest expectedItems []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", Options: &ListRequestOptions{}, }, 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", }, }, folders: []store.Folder{ {UID: "some_folder_1"}, {UID: "some_folder_2"}, }, list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Options: &ListRequestOptions{}, }, expectedItems: []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", }, }, folders: []store.Folder{ {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, {UID: "some_folder_subchild1", ParentUID: strPtr("some_folder_child")}, {UID: "some_folder_subchild2", ParentUID: strPtr("some_folder_child")}, {UID: "some_folder_subsubchild", ParentUID: strPtr("some_folder_subchild2")}, {UID: "some_folder_1", ParentUID: strPtr("some_other_folder")}, }, list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Options: &ListRequestOptions{}, }, 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", }, }, folders: []store.Folder{ {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Options: &ListRequestOptions{}, }, expectedItems: []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", }, }, folders: []store.Folder{ {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, {UID: "some_folder_subchild", ParentUID: strPtr("some_folder_child")}, {UID: "some_folder_child2", ParentUID: strPtr("some_folder_parent")}, }, list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Options: &ListRequestOptions{}, }, expectedFolders: []string{"some_folder_parent", "some_folder_child", "some_folder_child2", "some_folder_subchild"}, }, { name: "return no dashboards and folders if the user doesn't have access to any resources", permissions: []accesscontrol.Permission{}, folders: []store.Folder{ {UID: "some_folder_1"}, }, list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", Options: &ListRequestOptions{}, }, }, { name: "should collect folder permissions into items", permissions: []accesscontrol.Permission{ { Action: "folders:read", Scope: "folders:uid:some_folder_parent", Kind: "folders", Attribute: "uid", Identifier: "some_folder_parent", }, }, folders: []store.Folder{ {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, list: listRequest{ Action: "folders:read", Group: "folder.grafana.app", Resource: "folders", Options: &ListRequestOptions{}, }, expectedItems: []string{"some_folder_parent", "some_folder_child"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() if tc.folders != nil { s.folderCache.Set(context.Background(), folderCacheKey("default"), newFolderTree(tc.folders)) } tc.list.Namespace = types.NamespaceInfo{Value: "default", OrgID: 1} got, err := s.listPermission(context.Background(), s.getScopeMap(tc.permissions), &tc.list) require.NoError(t, err) assert.Equal(t, tc.expectedAll, got.All) assert.ElementsMatch(t, tc.expectedItems, got.Items) assert.ElementsMatch(t, tc.expectedFolders, got.Folders) }) } } func TestService_Check(t *testing.T) { callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"), Audience: []string{"authzservice"}, }, Rest: authn.AccessTokenClaims{Namespace: "org-12"}, }) type testCase struct { name string req *authzv1.CheckRequest permissions []accesscontrol.Permission expected bool expectErr bool } t.Run("Require auth info", func(t *testing.T) { s := setupService() ctx := context.Background() _, err := s.Check(ctx, &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }) require.Error(t, err) require.Contains(t, err.Error(), "could not get auth info") }) testCases := []testCase{ { name: "should error if no namespace is provided", req: &authzv1.CheckRequest{ Namespace: "", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expectErr: true, }, { name: "should error if caller namespace does not match request namespace", req: &authzv1.CheckRequest{ Namespace: "org-13", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expectErr: true, }, { name: "should error if no subject is provided", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expectErr: true, }, { name: "should error if an unsupported subject type is provided", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "api-key:12", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expectErr: true, }, { name: "should error if an invalid subject is provided", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "invalid:12", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expectErr: true, }, { name: "should error if an unknown group is provided", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "unknown.grafana.app", Resource: "unknown", Verb: "get", Name: "u1", }, expectErr: true, }, { name: "should error if an unknown verb is provided", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "unknown", Name: "u1", }, expectErr: true, }, } t.Run("Request validation", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{ userID: userID, userPermissions: tc.permissions, } s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{} _, err := s.Check(ctx, tc.req) require.Error(t, err) }) } }) testCases = []testCase{ { name: "should allow user with permission", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}}, expected: true, }, { name: "should deny user without permission", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}}, expected: false, }, { name: "should translate from id to uid based permissions", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "iam.grafana.app", Resource: "teams", Verb: "get", Name: "t1", }, permissions: []accesscontrol.Permission{ {Action: "teams:read", Scope: "teams:id:1"}, }, expected: true, }, { name: "should take into account action sets", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, permissions: []accesscontrol.Permission{ {Action: "dashboards:admin", Scope: "dashboards:uid:dash1"}, }, expected: true, }, { name: "should take into account folder action sets for dashboard access", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", Folder: "some_folder", }, permissions: []accesscontrol.Permission{ {Action: "folders:edit", Scope: "folders:uid:some_folder"}, }, expected: true, }, { name: "lower level action set or action set on a different resource should not grant higher level access", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "folder.grafana.app", Resource: "folders", Verb: "delete", Name: "folder1", }, permissions: []accesscontrol.Permission{ {Action: "folders:view", Scope: "folders:uid:folder1"}, {Action: "folders:edit", Scope: "folders:uid:other_folder"}, }, expected: false, }, { // We've had cases where permissions were saved to the database // without splitting the scope into 'kind', 'attribute', and 'identifier'. // Our wildcard check depends on this separation to work correctly. // This test makes sure we can still handle those unsplit permissions. name: "should split wildcard scope if needed", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "iam.grafana.app", Resource: "teams", Verb: "get", Name: "t1", }, permissions: []accesscontrol.Permission{ {Action: "teams:read", Scope: "teams:*"}, }, expected: true, }, } t.Run("User permission check", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{ userID: userID, userPermissions: tc.permissions, } s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{ teams: []team.Team{{ID: 1, UID: "t1", OrgID: 1}}, } resp, err := s.Check(ctx, tc.req) require.NoError(t, err) require.Equal(t, tc.expected, resp.Allowed) // Check cache id, ok := s.idCache.Get(ctx, userIdentifierCacheKey("org-12", "test-uid")) require.True(t, ok) require.Equal(t, id.UID, "test-uid") expAction := "dashboards:read" if tc.req.Resource == "teams" { expAction = "teams:read" } if tc.req.Resource == "folders" { expAction = "folders:delete" } perms, ok := s.permCache.Get(ctx, userPermCacheKey("org-12", "test-uid", expAction)) require.True(t, ok) require.Len(t, perms, 1) }) } }) testCases = []testCase{ { name: "should allow anonymous with permission", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "anonymous:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}}, expected: true, }, { name: "should deny anonymous without permission", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "anonymous:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}}, expected: false, }, } t.Run("Anonymous permission check", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) store := &fakeStore{userPermissions: tc.permissions} s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{} resp, err := s.Check(ctx, tc.req) require.NoError(t, err) assert.Equal(t, tc.expected, resp.Allowed) // Check cache perms, ok := s.permCache.Get(ctx, anonymousPermCacheKey("org-12", "dashboards:read")) require.True(t, ok) require.Len(t, perms, 1) }) } }) testCases = []testCase{ { name: "should allow rendering with permission", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "render:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expected: true, }, { name: "should deny rendering access to another app resources", req: &authzv1.CheckRequest{ Namespace: "org-12", Subject: "render:0", Group: "another.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }, expected: false, expectErr: true, }, } t.Run("Rendering permission check", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) resp, err := s.Check(ctx, tc.req) if tc.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tc.expected, resp.Allowed) }) } }) } func TestService_CacheCheck(t *testing.T) { callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"), Audience: []string{"authzservice"}, }, Rest: authn.AccessTokenClaims{Namespace: "org-12"}, }) ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} t.Run("Allow based on cached permissions", func(t *testing.T) { s := setupService() s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID) s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": true}) resp, err := s.Check(ctx, &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", }) require.NoError(t, err) assert.True(t, resp.Allowed) }) t.Run("Fallback to the database on cache miss", func(t *testing.T) { s := setupService() // Populate database permission but not the cache store := &fakeStore{ userID: userID, userPermissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}}, } s.store = store s.permissionStore = store s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID) resp, err := s.Check(ctx, &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash2", }) require.NoError(t, err) assert.True(t, resp.Allowed) }) t.Run("Fallback to the database on outdated cache", func(t *testing.T) { s := setupService() store := &fakeStore{ userID: userID, userPermissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}}, } s.store = store s.permissionStore = store s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID) // The cache does not have the permission for dash2 (outdated) s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": true}) resp, err := s.Check(ctx, &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash2", }) require.NoError(t, err) assert.True(t, resp.Allowed) }) t.Run("Should deny on explicit cache deny entry", func(t *testing.T) { s := setupService() s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID) // Explicitly deny access to the dashboard s.permDenialCache.Set(ctx, userPermDenialCacheKey("org-12", "test-uid", "dashboards:read", "dash1", "fold1"), true) // Allow access to the dashboard to prove this is not checked s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": false}) resp, err := s.Check(ctx, &authzv1.CheckRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", Name: "dash1", Folder: "fold1", }) require.NoError(t, err) assert.False(t, resp.Allowed) }) } func TestService_List(t *testing.T) { callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"), Audience: []string{"authzservice"}, }, Rest: authn.AccessTokenClaims{Namespace: "org-12"}, }) type testCase struct { name string req *authzv1.ListRequest permissions []accesscontrol.Permission expected *authzv1.ListResponse expectErr bool } t.Run("Require auth info", func(t *testing.T) { s := setupService() ctx := context.Background() _, err := s.List(ctx, &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }) require.Error(t, err) require.Contains(t, err.Error(), "could not get auth info") }) testCases := []testCase{ { name: "should error if no namespace is provided", req: &authzv1.ListRequest{ Namespace: "", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, { name: "should error if caller namespace does not match request namespace", req: &authzv1.ListRequest{ Namespace: "org-13", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, { name: "should error if no subject is provided", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, { name: "should error if an unsupported subject type is provided", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "api-key:12", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, { name: "should error if an invalid subject is provided", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "invalid:12", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, { name: "should error if an unknown group is provided", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "unknown.grafana.app", Resource: "unknown", Verb: "get", }, expectErr: true, }, { name: "should error if an unknown verb is provided", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "unknown", }, expectErr: true, }, } t.Run("Request validation", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{ userID: userID, userPermissions: tc.permissions, } s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{} _, err := s.List(ctx, tc.req) require.Error(t, err) }) } }) testCases = []testCase{ { name: "should list permissions for user with permission", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, {Action: "dashboards:read", Scope: "dashboards:uid:dash2"}, {Action: "dashboards:read", Scope: "folders:uid:fold1"}, }, expected: &authzv1.ListResponse{ Items: []string{"dash1", "dash2"}, Folders: []string{"fold1"}, }, }, { name: "should list permissions for user with permission or action set permissions", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, {Action: "dashboards:read", Scope: "dashboards:uid:dash2"}, {Action: "dashboards:read", Scope: "folders:uid:fold1"}, {Action: "dashboards:edit", Scope: "dashboards:uid:dash3"}, {Action: "folders:view", Scope: "folders:uid:fold2"}, }, expected: &authzv1.ListResponse{ Items: []string{"dash1", "dash2", "dash3"}, Folders: []string{"fold1", "fold2"}, }, }, { name: "should return empty list for user without permission", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, permissions: []accesscontrol.Permission{}, expected: &authzv1.ListResponse{}, }, } t.Run("User permission list", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} store := &fakeStore{ userID: userID, userPermissions: tc.permissions, } s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{} resp, err := s.List(ctx, tc.req) require.NoError(t, err) require.ElementsMatch(t, resp.Items, tc.expected.Items) require.ElementsMatch(t, resp.Folders, tc.expected.Folders) // Check cache id, ok := s.idCache.Get(ctx, userIdentifierCacheKey("org-12", "test-uid")) require.True(t, ok) require.Equal(t, id.UID, "test-uid") perms, ok := s.permCache.Get(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read")) require.True(t, ok) require.Len(t, perms, len(tc.expected.Items)+len(tc.expected.Folders)) }) } }) testCases = []testCase{ { name: "should list permissions for anonymous with permission", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "anonymous:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, {Action: "dashboards:read", Scope: "dashboards:uid:dash2"}, {Action: "dashboards:read", Scope: "folders:uid:fold1"}, }, expected: &authzv1.ListResponse{ Items: []string{"dash1", "dash2"}, Folders: []string{"fold1"}, }, }, { name: "should return empty list for anonymous without permission", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "anonymous:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, permissions: []accesscontrol.Permission{}, expected: &authzv1.ListResponse{}, }, } t.Run("Anonymous permission list", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) store := &fakeStore{userPermissions: tc.permissions} s.store = store s.permissionStore = store s.identityStore = &fakeIdentityStore{} resp, err := s.List(ctx, tc.req) require.NoError(t, err) require.ElementsMatch(t, resp.Items, tc.expected.Items) require.ElementsMatch(t, resp.Folders, tc.expected.Folders) // Check cache perms, ok := s.permCache.Get(ctx, anonymousPermCacheKey("org-12", "dashboards:read")) require.True(t, ok) require.Len(t, perms, len(tc.expected.Items)+len(tc.expected.Folders)) }) } }) testCases = []testCase{ { name: "should list permissions for rendering", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "render:0", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "get", }, expected: &authzv1.ListResponse{ All: true, }, }, { name: "should deny rendering access to another app resources", req: &authzv1.ListRequest{ Namespace: "org-12", Subject: "render:0", Group: "another.grafana.app", Resource: "dashboards", Verb: "get", }, expectErr: true, }, } t.Run("Rendering permission list", func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) resp, err := s.List(ctx, tc.req) if tc.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tc.expected.All, resp.All) }) } }) } func TestService_getAnonymousPermissions(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission action string expectedPerms map[string]bool expectedError bool anonRole string } testCases := []testCase{ { name: "should return permissions from store if not in cache", permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:some_dashboard"}, }, action: "dashboards:read", expectedPerms: map[string]bool{"dashboards:uid:some_dashboard": true}, expectedError: false, anonRole: "Viewer", }, { name: "should return error if store fails", permissions: nil, action: "dashboards:read", expectedPerms: nil, expectedError: true, anonRole: "Viewer", }, { name: "should handle wildcard permissions", permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "*", Kind: "*"}, }, action: "dashboards:read", expectedPerms: map[string]bool{"*": true}, expectedError: false, anonRole: "Viewer", }, { name: "should use custom anonymous role when specified", permissions: []accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:uid:some_dashboard"}, }, action: "dashboards:read", expectedPerms: map[string]bool{"dashboards:uid:some_dashboard": true}, expectedError: false, anonRole: "Editor", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := setupService() if tc.anonRole != "" { s.settings.AnonOrgRole = tc.anonRole } ns := types.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} store := &fakeStore{ userPermissions: tc.permissions, err: tc.expectedError, disableNsCheck: true, } s.store = store s.permissionStore = store perms, err := s.getAnonymousPermissions(ctx, ns, tc.action, []string{}) if tc.expectedError { require.Error(t, err) return } require.NoError(t, err) require.Equal(t, tc.expectedPerms, perms) // cache should then be set cached, ok := s.permCache.Get(ctx, anonymousPermCacheKey(ns.Value, tc.action)) require.True(t, ok) require.Equal(t, tc.expectedPerms, cached) }) } } func TestService_CacheList(t *testing.T) { callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"), Audience: []string{"authzservice"}, }, Rest: authn.AccessTokenClaims{Namespace: "org-12"}, }) t.Run("List based on cached permissions", func(t *testing.T) { s := setupService() ctx := types.WithAuthInfo(context.Background(), callingService) userID := &store.UserIdentifiers{UID: "test-uid", ID: 1} s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID) s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": true, "dashboards:uid:dash2": true, "folders:uid:fold1": true}, ) s.identityStore = &fakeIdentityStore{} resp, err := s.List(ctx, &authzv1.ListRequest{ Namespace: "org-12", Subject: "user:test-uid", Group: "dashboard.grafana.app", Resource: "dashboards", Verb: "list", }) require.NoError(t, err) require.ElementsMatch(t, resp.Items, []string{"dash1", "dash2"}) require.ElementsMatch(t, resp.Folders, []string{"fold1"}) }) } func setupService() *Service { cache := cache.NewLocalCache(cache.Config{Expiry: 5 * time.Minute, CleanupInterval: 5 * time.Minute}) logger := log.New("authz-rbac-service") fStore := &fakeStore{} tracer := tracing.NewNoopTracerService() return &Service{ logger: logger, mapper: NewMapperRegistry(), tracer: tracer, metrics: newMetrics(nil), idCache: newCacheWrap[store.UserIdentifiers](cache, logger, tracer, longCacheTTL), permCache: newCacheWrap[map[string]bool](cache, logger, tracer, shortCacheTTL), permDenialCache: newCacheWrap[bool](cache, logger, tracer, shortCacheTTL), userTeamCache: newCacheWrap[[]int64](cache, logger, tracer, shortCacheTTL), basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, tracer, longCacheTTL), folderCache: newCacheWrap[folderTree](cache, logger, tracer, shortCacheTTL), teamIDCache: newCacheWrap[map[int64]string](cache, logger, tracer, shortCacheTTL), settings: Settings{AnonOrgRole: "Viewer"}, store: fStore, permissionStore: fStore, folderStore: fStore, identityStore: &fakeIdentityStore{}, sf: new(singleflight.Group), } } func strPtr(s string) *string { return &s } type fakeStore struct { store.Store // The namespace has to be set in the handlers for the correct organization to be picked up. disableNsCheck bool folders []store.Folder basicRole *store.BasicRole userID *store.UserIdentifiers userPermissions []accesscontrol.Permission err bool calls int } func (f *fakeStore) GetBasicRoles(ctx context.Context, namespace types.NamespaceInfo, query store.BasicRoleQuery) (*store.BasicRole, error) { if ns, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && (!ok || ns != namespace.Value) { return nil, fmt.Errorf("namespace mismatch") } f.calls++ if f.err { return nil, fmt.Errorf("store error") } return f.basicRole, nil } func (f *fakeStore) GetUserIdentifiers(ctx context.Context, query store.UserIdentifierQuery) (*store.UserIdentifiers, error) { if _, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && !ok { return nil, fmt.Errorf("namespace not found") } f.calls++ if f.err { return nil, fmt.Errorf("store error") } return f.userID, nil } func (f *fakeStore) GetUserPermissions(ctx context.Context, namespace types.NamespaceInfo, query store.PermissionsQuery) ([]accesscontrol.Permission, error) { if ns, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && (!ok || ns != namespace.Value) { return nil, fmt.Errorf("namespace mismatch") } f.calls++ if f.err { return nil, fmt.Errorf("store error") } var permissions []accesscontrol.Permission for _, p := range f.userPermissions { if p.Action == query.Action || slices.Contains(query.ActionSets, p.Action) { permissions = append(permissions, p) } } return permissions, nil } func (f *fakeStore) ListFolders(ctx context.Context, namespace types.NamespaceInfo) ([]store.Folder, error) { if ns, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && (!ok || ns != namespace.Value) { return nil, fmt.Errorf("namespace mismatch") } f.calls++ if f.err { return nil, fmt.Errorf("store error") } return f.folders, nil } type fakeIdentityStore struct { legacy.LegacyIdentityStore userTeams []int64 teams []team.Team disableNsCheck bool err bool calls int } func (f *fakeIdentityStore) ListUserTeams(ctx context.Context, namespace types.NamespaceInfo, query legacy.ListUserTeamsQuery) (*legacy.ListUserTeamsResult, error) { if ns, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && (!ok || ns != namespace.Value) { return nil, fmt.Errorf("namespace mismatch") } f.calls++ if f.err { return nil, fmt.Errorf("identity store error") } items := make([]legacy.UserTeam, 0, len(f.userTeams)) for _, teamID := range f.userTeams { items = append(items, legacy.UserTeam{ID: teamID}) } return &legacy.ListUserTeamsResult{ Items: items, Continue: 0, }, nil } func (f *fakeIdentityStore) ListTeams(ctx context.Context, namespace types.NamespaceInfo, query legacy.ListTeamQuery) (*legacy.ListTeamResult, error) { if ns, ok := request.NamespaceFrom(ctx); !f.disableNsCheck && (!ok || ns != namespace.Value) { return nil, fmt.Errorf("namespace mismatch") } f.calls++ if f.err { return nil, fmt.Errorf("identity store error") } return &legacy.ListTeamResult{Teams: f.teams}, nil }