package dualwrite import ( "context" "fmt" "testing" "time" authlib "github.com/grafana/authlib/types" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" ) func TestMain(m *testing.M) { testsuite.Run(m) } type testEnv struct { sql *sqlstore.SQLStore db db.DB cfg *setting.Cfg userSvc user.Service teamSvc team.Service orgSvc org.Service folderSvc folder.Service ctx context.Context } func setupTestEnv(t *testing.T) *testEnv { t.Helper() sql, cfg := db.InitTestDBWithCfg(t) cfg.AutoAssignOrg = true cfg.AutoAssignOrgRole = "Viewer" cfg.AutoAssignOrgId = 1 teamService, err := teamimpl.ProvideService(sql, cfg, tracing.InitializeTracerForTest()) require.NoError(t, err) orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) require.NoError(t, err) userService, err := userimpl.ProvideService( sql, orgService, cfg, teamService, localcache.ProvideService(), tracing.InitializeTracerForTest(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), ) require.NoError(t, err) // Create test org orgID, err := orgService.GetOrCreate(context.Background(), "test") require.NoError(t, err) require.Equal(t, int64(1), orgID) return &testEnv{ sql: sql, db: sql, cfg: cfg, userSvc: userService, teamSvc: teamService, orgSvc: orgService, folderSvc: nil, // Set per test if needed ctx: context.Background(), } } func createUser(t *testing.T, env *testEnv, login string, orgID int64, isServiceAccount bool) *user.User { t.Helper() u, err := env.userSvc.Create(env.ctx, &user.CreateUserCommand{ Login: login, OrgID: orgID, IsServiceAccount: isServiceAccount, }) require.NoError(t, err) return u } func createTeam(t *testing.T, env *testEnv, name string, orgID int64) team.Team { t.Helper() tm, err := env.teamSvc.CreateTeam(env.ctx, &team.CreateTeamCommand{ Name: name, OrgID: orgID, }) require.NoError(t, err) return tm } func addTeamMember(t *testing.T, env *testEnv, userID, orgID, teamID int64, permission int) { t.Helper() err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { now := time.Now() _, err := sess.Exec("INSERT INTO team_member (org_id, team_id, user_id, permission, created, updated) VALUES (?, ?, ?, ?, ?, ?)", orgID, teamID, userID, permission, now, now) return err }) require.NoError(t, err) } func createRole(t *testing.T, env *testEnv, name string, orgID int64) string { t.Helper() var roleUID string err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { uid := fmt.Sprintf("role-%s-%d", name, orgID) now := time.Now() _, err := sess.Exec("INSERT INTO role (uid, name, org_id, version, created, updated) VALUES (?, ?, ?, ?, ?, ?)", uid, name, orgID, 1, now, now) roleUID = uid return err }) require.NoError(t, err) return roleUID } func addUserRole(t *testing.T, env *testEnv, userID int64, roleUID string, orgID int64) { t.Helper() err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { var roleID int64 _, err := sess.SQL("SELECT id FROM role WHERE uid = ?", roleUID).Get(&roleID) if err != nil { return err } _, err = sess.Exec("INSERT INTO user_role (org_id, user_id, role_id, created) VALUES (?, ?, ?, ?)", orgID, userID, roleID, time.Now()) return err }) require.NoError(t, err) } func addTeamRole(t *testing.T, env *testEnv, teamID int64, roleUID string, orgID int64) { t.Helper() err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { var roleID int64 _, err := sess.SQL("SELECT id FROM role WHERE uid = ?", roleUID).Get(&roleID) if err != nil { return err } _, err = sess.Exec("INSERT INTO team_role (org_id, team_id, role_id, created) VALUES (?, ?, ?, ?)", orgID, teamID, roleID, time.Now()) return err }) require.NoError(t, err) } func addPermission(t *testing.T, env *testEnv, roleUID, action, kind, identifier string) { t.Helper() err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { var roleID int64 _, err := sess.SQL("SELECT id FROM role WHERE uid = ?", roleUID).Get(&roleID) if err != nil { return err } now := time.Now() _, err = sess.Exec("INSERT INTO permission (role_id, action, scope, kind, attribute, identifier, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", roleID, action, fmt.Sprintf("%s:uid:%s", kind, identifier), kind, "uid", identifier, now, now) return err }) require.NoError(t, err) } func addBuiltinRole(t *testing.T, env *testEnv, roleUID, builtinRole string, orgID int64) { t.Helper() err := env.db.WithDbSession(env.ctx, func(sess *db.Session) error { var roleID int64 _, err := sess.SQL("SELECT id FROM role WHERE uid = ?", roleUID).Get(&roleID) if err != nil { return err } now := time.Now() _, err = sess.Exec("INSERT INTO builtin_role (role_id, org_id, role, created, updated) VALUES (?, ?, ?, ?, ?)", roleID, orgID, builtinRole, now, now) return err }) require.NoError(t, err) } // Mock zanzana client for testing type mockZanzanaClient struct { mock.Mock } func (m *mockZanzanaClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) { args := m.Called(ctx, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*authzextv1.ReadResponse), args.Error(1) } func (m *mockZanzanaClient) Write(ctx context.Context, req *authzextv1.WriteRequest) error { args := m.Called(ctx, req) return args.Error(0) } func (m *mockZanzanaClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { args := m.Called(ctx, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*authzextv1.BatchCheckResponse), args.Error(1) } // authlib.AccessClient methods func (m *mockZanzanaClient) Check(ctx context.Context, id authlib.AuthInfo, req authlib.CheckRequest, folder string) (authlib.CheckResponse, error) { args := m.Called(ctx, id, req, folder) return args.Get(0).(authlib.CheckResponse), args.Error(1) } func (m *mockZanzanaClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) { args := m.Called(ctx, id, req) if args.Get(0) == nil { return nil, nil, args.Error(2) } return args.Get(0).(authlib.ItemChecker), args.Get(1).(authlib.Zookie), args.Error(2) } func (m *mockZanzanaClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { return nil } func (m *mockZanzanaClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { args := m.Called(ctx, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*authzextv1.QueryResponse), args.Error(1) } func TestIntegrationTeamMembershipCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect team members with member permission", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) team1 := createTeam(t, env, "team1", 1) addTeamMember(t, env, user1.ID, 1, team1.ID, 0) // 0 = member permission collector := teamMembershipCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 1) teamObject := zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, "") require.Contains(t, tuples, teamObject) teamTuples := tuples[teamObject] require.Len(t, teamTuples, 1) var tuple *openfgav1.TupleKey for _, t := range teamTuples { tuple = t break } assert.Equal(t, zanzana.NewTupleEntry(zanzana.TypeUser, user1.UID, ""), tuple.User) assert.Equal(t, zanzana.RelationTeamMember, tuple.Relation) assert.Equal(t, teamObject, tuple.Object) }) t.Run("should collect team admins with admin permission", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) team1 := createTeam(t, env, "team1", 1) addTeamMember(t, env, user1.ID, 1, team1.ID, 4) // 4 = admin permission collector := teamMembershipCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 1) teamObject := zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, "") teamTuples := tuples[teamObject] var tuple *openfgav1.TupleKey for _, t := range teamTuples { tuple = t break } assert.Equal(t, zanzana.RelationTeamAdmin, tuple.Relation) }) t.Run("should collect multiple team members", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) user2 := createUser(t, env, "user2", 1, false) team1 := createTeam(t, env, "team1", 1) addTeamMember(t, env, user1.ID, 1, team1.ID, 0) addTeamMember(t, env, user2.ID, 1, team1.ID, 4) collector := teamMembershipCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) teamObject := zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, "") teamTuples := tuples[teamObject] require.Len(t, teamTuples, 2) }) t.Run("should return empty for no team memberships", func(t *testing.T) { env := setupTestEnv(t) collector := teamMembershipCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Empty(t, tuples) }) t.Run("should filter by org ID", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) team1 := createTeam(t, env, "team1", 1) addTeamMember(t, env, user1.ID, 1, team1.ID, 0) // Collect for org 1 only collector := teamMembershipCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 1) teamObject := zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, "") require.Contains(t, tuples, teamObject) }) } func TestIntegrationFolderTreeCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect folder parent relationships", func(t *testing.T) { env := setupTestEnv(t) fakeFolderSvc := foldertest.NewFakeService() fakeFolderSvc.ExpectedFolders = []*folder.Folder{ {UID: "child1", ParentUID: "parent1", OrgID: 1}, {UID: "child2", ParentUID: "parent1", OrgID: 1}, } collector := folderTreeCollector(fakeFolderSvc) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 2) child1Object := zanzana.NewTupleEntry(zanzana.TypeFolder, "child1", "") require.Contains(t, tuples, child1Object) child1Tuples := tuples[child1Object] require.Len(t, child1Tuples, 1) var tuple *openfgav1.TupleKey for _, t := range child1Tuples { tuple = t break } assert.Equal(t, child1Object, tuple.Object) assert.Equal(t, zanzana.RelationParent, tuple.Relation) assert.Equal(t, zanzana.NewTupleEntry(zanzana.TypeFolder, "parent1", ""), tuple.User) }) t.Run("should skip folders without parents", func(t *testing.T) { env := setupTestEnv(t) fakeFolderSvc := foldertest.NewFakeService() fakeFolderSvc.ExpectedFolders = []*folder.Folder{ {UID: "root1", ParentUID: "", OrgID: 1}, {UID: "child1", ParentUID: "root1", OrgID: 1}, } collector := folderTreeCollector(fakeFolderSvc) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 1) // Only child1 should be collected child1Object := zanzana.NewTupleEntry(zanzana.TypeFolder, "child1", "") require.Contains(t, tuples, child1Object) }) t.Run("should handle pagination", func(t *testing.T) { env := setupTestEnv(t) fakeFolderSvc := foldertest.NewFakeService() // Create 250 folders to test pagination var folders []*folder.Folder for i := 1; i <= 250; i++ { folders = append(folders, &folder.Folder{ UID: fmt.Sprintf("child%d", i), ParentUID: "parent1", OrgID: 1, }) } // Mock GetFolders to return pages fakeFolderSvc.ExpectedFolders = folders collector := folderTreeCollector(fakeFolderSvc) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Len(t, tuples, 250) }) t.Run("should return empty for no folders", func(t *testing.T) { env := setupTestEnv(t) fakeFolderSvc := foldertest.NewFakeService() fakeFolderSvc.ExpectedFolders = []*folder.Folder{} collector := folderTreeCollector(fakeFolderSvc) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Empty(t, tuples) }) t.Run("should handle folder service error", func(t *testing.T) { env := setupTestEnv(t) fakeFolderSvc := foldertest.NewFakeService() fakeFolderSvc.ExpectedError = fmt.Errorf("folder service error") collector := folderTreeCollector(fakeFolderSvc) _, err := collector(env.ctx, 1) require.Error(t, err) }) } func TestIntegrationBasicRoleBindingsCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect basic role bindings for regular users", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) // Update org role err := env.orgSvc.UpdateOrgUser(env.ctx, &org.UpdateOrgUserCommand{ OrgID: 1, UserID: user1.ID, Role: org.RoleAdmin, }) require.NoError(t, err) collector := basicRoleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) roleObject := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole("Admin"), "") require.Contains(t, tuples, roleObject) roleTuples := tuples[roleObject] require.NotEmpty(t, roleTuples) var found bool for _, tuple := range roleTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeUser, user1.UID, "") { assert.Equal(t, zanzana.RelationAssignee, tuple.Relation) found = true break } } assert.True(t, found) }) t.Run("should collect basic role bindings for service accounts", func(t *testing.T) { env := setupTestEnv(t) sa := createUser(t, env, "sa1", 1, true) collector := basicRoleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) // Find service account tuple var found bool for _, roleTuples := range tuples { for _, tuple := range roleTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeServiceAccount, sa.UID, "") { found = true break } } } assert.True(t, found) }) t.Run("should collect multiple users with different roles", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) user2 := createUser(t, env, "user2", 1, false) err := env.orgSvc.UpdateOrgUser(env.ctx, &org.UpdateOrgUserCommand{ OrgID: 1, UserID: user1.ID, Role: org.RoleAdmin, }) require.NoError(t, err) err = env.orgSvc.UpdateOrgUser(env.ctx, &org.UpdateOrgUserCommand{ OrgID: 1, UserID: user2.ID, Role: org.RoleEditor, }) require.NoError(t, err) collector := basicRoleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) adminRole := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole("Admin"), "") editorRole := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole("Editor"), "") require.Contains(t, tuples, adminRole) require.Contains(t, tuples, editorRole) }) } func TestIntegrationManagedPermissionsCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect managed permissions for users", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) roleUID := createRole(t, env, "managed:dash:u1:perm", 1) addUserRole(t, env, user1.ID, roleUID, 1) addPermission(t, env, roleUID, "dashboards:read", "dashboards", "uid1") collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) }) t.Run("should collect managed permissions for service accounts", func(t *testing.T) { env := setupTestEnv(t) sa := createUser(t, env, "sa1", 1, true) roleUID := createRole(t, env, "managed:dash:u1:perm", 1) addUserRole(t, env, sa.ID, roleUID, 1) addPermission(t, env, roleUID, "dashboards:read", "dashboards", "uid1") collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) // Verify service account subject var found bool for _, objTuples := range tuples { for _, tuple := range objTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeServiceAccount, sa.UID, "") { found = true break } } } assert.True(t, found, "Should find service account tuple") }) t.Run("should collect managed permissions for teams", func(t *testing.T) { env := setupTestEnv(t) team1 := createTeam(t, env, "team1", 1) roleUID := createRole(t, env, "managed:dash:u1:perm", 1) addTeamRole(t, env, team1.ID, roleUID, 1) addPermission(t, env, roleUID, "dashboards:read", "dashboards", "uid1") collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) // Verify team subject var found bool for _, objTuples := range tuples { for _, tuple := range objTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, zanzana.RelationTeamMember) { found = true break } } } assert.True(t, found, "Should find team tuple") }) t.Run("should collect managed permissions for basic roles", func(t *testing.T) { env := setupTestEnv(t) roleUID := createRole(t, env, "managed:dash:u1:perm", 1) addBuiltinRole(t, env, roleUID, "Admin", 1) addPermission(t, env, roleUID, "dashboards:read", "dashboards", "uid1") collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) }) t.Run("should filter by kind", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) roleUID1 := createRole(t, env, "managed:dash:u1:perm", 1) roleUID2 := createRole(t, env, "managed:fld:u2:perm", 1) addUserRole(t, env, user1.ID, roleUID1, 1) addUserRole(t, env, user1.ID, roleUID2, 1) addPermission(t, env, roleUID1, "dashboards:read", "dashboards", "uid1") addPermission(t, env, roleUID2, "folders:read", "folders", "uid2") // Collect only dashboards collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) // Should only have dashboard tuples for _, objTuples := range tuples { for _, tuple := range objTuples { assert.NotContains(t, tuple.Object, "folders") } } }) t.Run("should return empty for no managed permissions", func(t *testing.T) { env := setupTestEnv(t) collector := managedPermissionsCollector(env.db, "dashboards") tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Empty(t, tuples) }) } func TestIntegrationRoleBindingsCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect user role bindings", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) roleUID := createRole(t, env, "custom-role", 1) addUserRole(t, env, user1.ID, roleUID, 1) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) roleObject := zanzana.NewTupleEntry(zanzana.TypeRole, roleUID, "") require.Contains(t, tuples, roleObject) roleTuples := tuples[roleObject] require.NotEmpty(t, roleTuples) var found bool for _, tuple := range roleTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeUser, user1.UID, "") && tuple.Relation == zanzana.RelationAssignee { found = true break } } assert.True(t, found) }) t.Run("should collect team role bindings", func(t *testing.T) { env := setupTestEnv(t) team1 := createTeam(t, env, "team1", 1) roleUID := createRole(t, env, "custom-role", 1) addTeamRole(t, env, team1.ID, roleUID, 1) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) roleObject := zanzana.NewTupleEntry(zanzana.TypeRole, roleUID, "") roleTuples := tuples[roleObject] var found bool for _, tuple := range roleTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeTeam, team1.UID, zanzana.RelationTeamMember) { found = true break } } assert.True(t, found) }) t.Run("should collect service account role bindings", func(t *testing.T) { env := setupTestEnv(t) sa := createUser(t, env, "sa1", 1, true) roleUID := createRole(t, env, "custom-role", 1) addUserRole(t, env, sa.ID, roleUID, 1) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) var found bool for _, roleTuples := range tuples { for _, tuple := range roleTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeServiceAccount, sa.UID, "") { found = true break } } } assert.True(t, found) }) t.Run("should filter out managed roles", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) customRole := createRole(t, env, "custom-role", 1) managedRole := createRole(t, env, "managed:dash:perm", 1) addUserRole(t, env, user1.ID, customRole, 1) addUserRole(t, env, user1.ID, managedRole, 1) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) // Should have custom role customRoleObject := zanzana.NewTupleEntry(zanzana.TypeRole, customRole, "") require.Contains(t, tuples, customRoleObject) // Should NOT have managed role managedRoleObject := zanzana.NewTupleEntry(zanzana.TypeRole, managedRole, "") require.NotContains(t, tuples, managedRoleObject) }) t.Run("should include global roles (org_id=0)", func(t *testing.T) { env := setupTestEnv(t) user1 := createUser(t, env, "user1", 1, false) globalRole := createRole(t, env, "global-role", 0) addUserRole(t, env, user1.ID, globalRole, 0) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) globalRoleObject := zanzana.NewTupleEntry(zanzana.TypeRole, globalRole, "") require.Contains(t, tuples, globalRoleObject) }) t.Run("should return empty for no role bindings", func(t *testing.T) { env := setupTestEnv(t) collector := roleBindingsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Empty(t, tuples) }) } func TestIntegrationRolePermissionsCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should collect role permissions", func(t *testing.T) { env := setupTestEnv(t) roleUID := createRole(t, env, "custom-role", 1) addPermission(t, env, roleUID, "dashboards:read", "dashboards", "uid1") collector := rolePermissionsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) }) t.Run("should filter out managed roles", func(t *testing.T) { env := setupTestEnv(t) customRole := createRole(t, env, "custom-role", 1) managedRole := createRole(t, env, "managed:dash:perm", 1) addPermission(t, env, customRole, "dashboards:read", "dashboards", "uid1") addPermission(t, env, managedRole, "dashboards:read", "dashboards", "uid2") collector := rolePermissionsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) // Should only have custom role permissions foundCustom := false foundManaged := false for _, objTuples := range tuples { for _, tuple := range objTuples { if tuple.User == zanzana.NewTupleEntry(zanzana.TypeRole, customRole, zanzana.RelationAssignee) { foundCustom = true } if tuple.User == zanzana.NewTupleEntry(zanzana.TypeRole, managedRole, zanzana.RelationAssignee) { foundManaged = true } } } assert.True(t, foundCustom, "Should find custom role permissions") assert.False(t, foundManaged, "Should not find managed role permissions") }) t.Run("should include global role permissions (org_id=0)", func(t *testing.T) { env := setupTestEnv(t) globalRole := createRole(t, env, "global-role", 0) addPermission(t, env, globalRole, "dashboards:read", "dashboards", "uid1") collector := rolePermissionsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.NotEmpty(t, tuples) }) t.Run("should return empty for no permissions", func(t *testing.T) { env := setupTestEnv(t) collector := rolePermissionsCollector(env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) require.Empty(t, tuples) }) } func TestIntegrationAnonymousRoleBindingsCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) t.Run("should return object with empty tuples when org doesn't match", func(t *testing.T) { env := setupTestEnv(t) cfg := setting.NewCfg() cfg.Anonymous.Enabled = true cfg.Anonymous.OrgName = "different-org" cfg.Anonymous.OrgRole = "Viewer" collector := anonymousRoleBindingsCollector(cfg, env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) // Should have object set but no tuples roleObject := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole("Viewer"), "") require.Contains(t, tuples, roleObject) roleTuples := tuples[roleObject] require.Empty(t, roleTuples) }) t.Run("should handle non-existent org gracefully", func(t *testing.T) { env := setupTestEnv(t) cfg := setting.NewCfg() cfg.Anonymous.Enabled = true cfg.Anonymous.OrgName = "non-existent-org" cfg.Anonymous.OrgRole = "Viewer" collector := anonymousRoleBindingsCollector(cfg, env.db) tuples, err := collector(env.ctx, 1) require.NoError(t, err) roleObject := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole("Viewer"), "") require.Contains(t, tuples, roleObject) roleTuples := tuples[roleObject] require.Empty(t, roleTuples) }) } func TestZanzanaCollector(t *testing.T) { t.Run("should collect tuples for single relation", func(t *testing.T) { mockClient := new(mockZanzanaClient) tuples := []*authzextv1.Tuple{ { Key: &authzextv1.TupleKey{ User: "user:user1", Relation: "member", Object: "team:team1", }, }, } mockClient.On("Read", mock.Anything, mock.MatchedBy(func(req *authzextv1.ReadRequest) bool { return req.TupleKey.Object == "team:team1" && req.TupleKey.Relation == "member" })).Return(&authzextv1.ReadResponse{ Tuples: tuples, ContinuationToken: "", }, nil) collector := zanzanaCollector([]string{"member"}) result, err := collector(context.Background(), mockClient, "team:team1", "org:1") require.NoError(t, err) require.Len(t, result, 1) mockClient.AssertExpectations(t) }) t.Run("should collect tuples for multiple relations", func(t *testing.T) { mockClient := new(mockZanzanaClient) memberTuples := []*authzextv1.Tuple{ { Key: &authzextv1.TupleKey{ User: "user:user1", Relation: "member", Object: "team:team1", }, }, } adminTuples := []*authzextv1.Tuple{ { Key: &authzextv1.TupleKey{ User: "user:user2", Relation: "admin", Object: "team:team1", }, }, } mockClient.On("Read", mock.Anything, mock.MatchedBy(func(req *authzextv1.ReadRequest) bool { return req.TupleKey.Relation == "member" })).Return(&authzextv1.ReadResponse{ Tuples: memberTuples, ContinuationToken: "", }, nil) mockClient.On("Read", mock.Anything, mock.MatchedBy(func(req *authzextv1.ReadRequest) bool { return req.TupleKey.Relation == "admin" })).Return(&authzextv1.ReadResponse{ Tuples: adminTuples, ContinuationToken: "", }, nil) collector := zanzanaCollector([]string{"member", "admin"}) result, err := collector(context.Background(), mockClient, "team:team1", "org:1") require.NoError(t, err) require.Len(t, result, 2) mockClient.AssertExpectations(t) }) t.Run("should handle pagination with continuation token", func(t *testing.T) { mockClient := new(mockZanzanaClient) firstPage := []*authzextv1.Tuple{ { Key: &authzextv1.TupleKey{ User: "user:user1", Relation: "member", Object: "team:team1", }, }, } secondPage := []*authzextv1.Tuple{ { Key: &authzextv1.TupleKey{ User: "user:user2", Relation: "member", Object: "team:team1", }, }, } // First call returns continuation token mockClient.On("Read", mock.Anything, mock.MatchedBy(func(req *authzextv1.ReadRequest) bool { return req.ContinuationToken == "" })).Return(&authzextv1.ReadResponse{ Tuples: firstPage, ContinuationToken: "token1", }, nil).Once() // Second call with continuation token mockClient.On("Read", mock.Anything, mock.MatchedBy(func(req *authzextv1.ReadRequest) bool { return req.ContinuationToken == "token1" })).Return(&authzextv1.ReadResponse{ Tuples: secondPage, ContinuationToken: "", }, nil).Once() collector := zanzanaCollector([]string{"member"}) result, err := collector(context.Background(), mockClient, "team:team1", "org:1") require.NoError(t, err) require.Len(t, result, 2) mockClient.AssertExpectations(t) }) t.Run("should return empty for no tuples", func(t *testing.T) { mockClient := new(mockZanzanaClient) mockClient.On("Read", mock.Anything, mock.Anything).Return(&authzextv1.ReadResponse{ Tuples: []*authzextv1.Tuple{}, ContinuationToken: "", }, nil) collector := zanzanaCollector([]string{"member"}) result, err := collector(context.Background(), mockClient, "team:team1", "org:1") require.NoError(t, err) require.Empty(t, result) mockClient.AssertExpectations(t) }) t.Run("should handle client error", func(t *testing.T) { mockClient := new(mockZanzanaClient) mockClient.On("Read", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("client error")) collector := zanzanaCollector([]string{"member"}) _, err := collector(context.Background(), mockClient, "team:team1", "org:1") require.Error(t, err) mockClient.AssertExpectations(t) }) }