Files
grafana/pkg/services/accesscontrol/dualwrite/collectors_test.go
T

1087 lines
31 KiB
Go

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