1087 lines
31 KiB
Go
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)
|
|
})
|
|
}
|