mirror of https://github.com/grafana/grafana.git
953 lines
28 KiB
Go
953 lines
28 KiB
Go
package iam
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAfterCoreRoleCreate(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Test Role",
|
|
Description: "Test role for folders",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testCoreRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
require.Equal(t, "org-1", req.Namespace)
|
|
|
|
expectedTuples := []*v1.TupleKey{
|
|
{User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"},
|
|
{User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"},
|
|
}
|
|
|
|
requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples)
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleEntries}
|
|
b.AfterRoleCreate(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should create zanzana entries for core role with dashboard permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "dashboard-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Dashboard Role",
|
|
Description: "Test role for dashboards",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
|
{Action: "dashboards:write", Scope: "dashboards:uid:dash1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testDashboardRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
require.Equal(t, "default", req.Namespace)
|
|
|
|
// Check subject is role with assignee relation
|
|
for _, tuple := range req.Writes.TupleKeys {
|
|
require.Equal(t, "role:dashboard-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "resource:")
|
|
require.Contains(t, tuple.Object, "dashboard")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleEntries}
|
|
b.AfterRoleCreate(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle wildcard scopes", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "wildcard-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Wildcard Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testWildcardEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 1)
|
|
|
|
tuple := req.Writes.TupleKeys[0]
|
|
require.Equal(t, "role:wildcard-role-uid#assignee", tuple.User)
|
|
// Wildcard should create a group_resource tuple
|
|
require.Contains(t, tuple.Object, "group_resource:")
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testWildcardEntries}
|
|
b.AfterRoleCreate(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should skip untranslatable permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "mixed-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Mixed Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "unknown:action", Scope: "unknown:scope"}, // This should be skipped
|
|
},
|
|
},
|
|
}
|
|
|
|
testMixedEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
// Should only have 1 tuple (the untranslatable one should be skipped)
|
|
require.Len(t, req.Writes.TupleKeys, 1)
|
|
|
|
tuple := req.Writes.TupleKeys[0]
|
|
require.Equal(t, "role:mixed-role-uid#assignee", tuple.User)
|
|
require.Equal(t, "folder:folder1", tuple.Object)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testMixedEntries}
|
|
b.AfterRoleCreate(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestAfterRoleCreate(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should create zanzana entries for role with folder permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "custom-role-uid",
|
|
Namespace: "org-3",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Custom Role",
|
|
Description: "Custom role for folders",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder2"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder2"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
require.Equal(t, "org-3", req.Namespace)
|
|
|
|
expectedTuples := []*v1.TupleKey{
|
|
{User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder2"},
|
|
{User: "role:custom-role-uid#assignee", Relation: "delete", Object: "folder:folder2"},
|
|
}
|
|
|
|
requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples)
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testRoleEntries}
|
|
b.AfterRoleCreate(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should create zanzana entries for role with dashboard permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "dash-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Dashboard Custom Role",
|
|
Description: "Custom role for dashboards",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:mydash"},
|
|
{Action: "dashboards:delete", Scope: "dashboards:uid:mydash"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testDashRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
require.Equal(t, "default", req.Namespace)
|
|
|
|
// Check subject is role with assignee relation
|
|
for _, tuple := range req.Writes.TupleKeys {
|
|
require.Equal(t, "role:dash-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "resource:")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleEntries}
|
|
b.AfterRoleCreate(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should merge folder resource tuples with same object and user", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "merge-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Merge Test Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
// These should create folder resource tuples that get merged
|
|
{Action: "dashboards:read", Scope: "folders:uid:parent-folder"},
|
|
{Action: "dashboards:write", Scope: "folders:uid:parent-folder"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testMergedEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Writes)
|
|
// After merging, we should have tuples for the folder resource actions
|
|
require.Greater(t, len(req.Writes.TupleKeys), 0)
|
|
|
|
for _, tuple := range req.Writes.TupleKeys {
|
|
require.Equal(t, "role:merge-role-uid#assignee", tuple.User)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testMergedEntries}
|
|
b.AfterRoleCreate(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestBeginCoreRoleUpdate(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should update zanzana entries when permissions change", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Test Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Test Role Updated",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder2"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder2"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testUpdate := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "org-1", req.Namespace)
|
|
|
|
// Verify deletes (old permissions)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
|
|
expectedDeletes := []*v1.TupleKeyWithoutCondition{
|
|
{User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"},
|
|
{User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"},
|
|
}
|
|
requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes)
|
|
|
|
// Verify writes (new permissions)
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
|
|
expectedWrites := []*v1.TupleKey{
|
|
{User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder2"},
|
|
{User: "role:test-role-uid#assignee", Relation: "delete", Object: "folder:folder2"},
|
|
}
|
|
requireTuplesMatch(t, req.Writes.TupleKeys, expectedWrites)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testUpdate}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle adding new permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "expand-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Expand Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "expand-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Expand Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testExpand := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "org-2", req.Namespace)
|
|
|
|
// Should delete old permission
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 1)
|
|
|
|
// Should write all new permissions
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 3)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testExpand}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle removing all permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "clear-role-uid",
|
|
Namespace: "org-3",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Clear Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "clear-role-uid",
|
|
Namespace: "org-3",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Clear Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{},
|
|
},
|
|
}
|
|
|
|
testClear := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "org-3", req.Namespace)
|
|
|
|
// Should delete old permissions
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
|
|
// Should have no writes
|
|
require.Nil(t, req.Writes)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testClear}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestBeginRoleUpdate(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should update zanzana entries when permissions change", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "custom-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Custom Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "custom-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Custom Role Updated",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
|
{Action: "dashboards:write", Scope: "dashboards:uid:dash1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testUpdate := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "org-1", req.Namespace)
|
|
|
|
// Verify deletes (old permissions)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
|
|
expectedDeletes := []*v1.TupleKeyWithoutCondition{
|
|
{User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder1"},
|
|
{User: "role:custom-role-uid#assignee", Relation: "update", Object: "folder:folder1"},
|
|
}
|
|
requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes)
|
|
|
|
// Verify writes (new permissions) - dashboards use resource type
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
|
|
// All writes should be for dashboards
|
|
for _, tuple := range req.Writes.TupleKeys {
|
|
require.Equal(t, "role:custom-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "resource:")
|
|
require.Contains(t, tuple.Object, "dashboard")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testUpdate}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle completely new permission set", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "swap-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Swap Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "swap-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Swap Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:write", Scope: "folders:uid:folder2"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder2"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testSwap := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "default", req.Namespace)
|
|
|
|
// Should delete old permission
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 1)
|
|
require.Equal(t, "role:swap-role-uid#assignee", req.Deletes.TupleKeys[0].User)
|
|
require.Equal(t, "folder:folder1", req.Deletes.TupleKeys[0].Object)
|
|
|
|
// Should write new permissions
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 2)
|
|
for _, tuple := range req.Writes.TupleKeys {
|
|
require.Equal(t, "role:swap-role-uid#assignee", tuple.User)
|
|
require.Equal(t, "folder:folder2", tuple.Object)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testSwap}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle adding permissions to empty role", func(t *testing.T) {
|
|
wg.Add(1)
|
|
oldRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "empty-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Empty Role",
|
|
Permissions: []iamv0.RolespecPermission{},
|
|
},
|
|
}
|
|
|
|
newRole := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "empty-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Empty Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testAddToEmpty := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.Equal(t, "org-2", req.Namespace)
|
|
|
|
// Should have no deletes
|
|
require.Nil(t, req.Deletes)
|
|
|
|
// Should write new permission
|
|
require.NotNil(t, req.Writes)
|
|
require.Len(t, req.Writes.TupleKeys, 1)
|
|
require.Equal(t, "role:empty-role-uid#assignee", req.Writes.TupleKeys[0].User)
|
|
require.Equal(t, "folder:folder1", req.Writes.TupleKeys[0].Object)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testAddToEmpty}
|
|
|
|
// Call BeginUpdate which does all the work
|
|
finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, finishFunc)
|
|
|
|
// Call the finish function with success=true to trigger the zanzana write
|
|
finishFunc(context.Background(), true)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestAfterCoreRoleDelete(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should delete zanzana entries for core role with folder permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Test Role",
|
|
Description: "Test role for folders",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testCoreRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
require.Equal(t, "org-1", req.Namespace)
|
|
|
|
expectedDeletes := []*v1.TupleKeyWithoutCondition{
|
|
{User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"},
|
|
{User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"},
|
|
}
|
|
|
|
requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes)
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleDeletes}
|
|
b.AfterRoleDelete(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should delete zanzana entries for core role with dashboard permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "dashboard-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Dashboard Role",
|
|
Description: "Test role for dashboards",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
|
{Action: "dashboards:write", Scope: "dashboards:uid:dash1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testDashboardRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
require.Equal(t, "default", req.Namespace)
|
|
|
|
// Check all deletes have the correct subject
|
|
for _, tuple := range req.Deletes.TupleKeys {
|
|
require.Equal(t, "role:dashboard-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "resource:")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleDeletes}
|
|
b.AfterRoleDelete(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle wildcard scopes on delete", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "wildcard-role-uid",
|
|
Namespace: "org-2",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Wildcard Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testWildcardDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 1)
|
|
|
|
tuple := req.Deletes.TupleKeys[0]
|
|
require.Equal(t, "role:wildcard-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "group_resource:")
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testWildcardDeletes}
|
|
b.AfterRoleDelete(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should skip untranslatable permissions on delete", func(t *testing.T) {
|
|
wg.Add(1)
|
|
coreRole := iamv0.CoreRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "mixed-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.CoreRoleSpec{
|
|
Title: "Mixed Role",
|
|
Permissions: []iamv0.CoreRolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "unknown:action", Scope: "unknown:scope"}, // This should be skipped
|
|
},
|
|
},
|
|
}
|
|
|
|
testMixedDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
// Should only delete 1 tuple (the untranslatable one should be skipped)
|
|
require.Len(t, req.Deletes.TupleKeys, 1)
|
|
|
|
tuple := req.Deletes.TupleKeys[0]
|
|
require.Equal(t, "role:mixed-role-uid#assignee", tuple.User)
|
|
require.Equal(t, "folder:folder1", tuple.Object)
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testMixedDeletes}
|
|
b.AfterRoleDelete(&coreRole, nil)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestAfterRoleDelete(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
b := &IdentityAccessManagementAPIBuilder{
|
|
logger: log.NewNopLogger(),
|
|
zTickets: make(chan bool, 1),
|
|
}
|
|
t.Run("should delete zanzana entries for role with folder permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "custom-role-uid",
|
|
Namespace: "org-3",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Custom Role",
|
|
Description: "Custom role for folders",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder2"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder2"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
require.Equal(t, "org-3", req.Namespace)
|
|
|
|
expectedDeletes := []*v1.TupleKeyWithoutCondition{
|
|
{User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder2"},
|
|
{User: "role:custom-role-uid#assignee", Relation: "delete", Object: "folder:folder2"},
|
|
}
|
|
|
|
requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes)
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testRoleDeletes}
|
|
b.AfterRoleDelete(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should delete zanzana entries for role with dashboard permissions", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "dash-role-uid",
|
|
Namespace: "default",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Dashboard Custom Role",
|
|
Description: "Custom role for dashboards",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:mydash"},
|
|
{Action: "dashboards:delete", Scope: "dashboards:uid:mydash"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testDashRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 2)
|
|
require.Equal(t, "default", req.Namespace)
|
|
|
|
// Check all deletes have the correct subject
|
|
for _, tuple := range req.Deletes.TupleKeys {
|
|
require.Equal(t, "role:dash-role-uid#assignee", tuple.User)
|
|
require.Contains(t, tuple.Object, "resource:")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleDeletes}
|
|
b.AfterRoleDelete(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should handle multiple permissions on delete", func(t *testing.T) {
|
|
wg.Add(1)
|
|
role := iamv0.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "multi-role-uid",
|
|
Namespace: "org-1",
|
|
},
|
|
Spec: iamv0.RoleSpec{
|
|
Title: "Multi Permission Role",
|
|
Permissions: []iamv0.RolespecPermission{
|
|
{Action: "folders:read", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:write", Scope: "folders:uid:folder1"},
|
|
{Action: "folders:delete", Scope: "folders:uid:folder1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
testMultiDeletes := func(ctx context.Context, req *v1.WriteRequest) error {
|
|
defer wg.Done()
|
|
require.NotNil(t, req)
|
|
require.NotNil(t, req.Deletes)
|
|
require.Len(t, req.Deletes.TupleKeys, 3)
|
|
|
|
// All should be for the same role and folder
|
|
for _, tuple := range req.Deletes.TupleKeys {
|
|
require.Equal(t, "role:multi-role-uid#assignee", tuple.User)
|
|
require.Equal(t, "folder:folder1", tuple.Object)
|
|
}
|
|
|
|
// Check all expected relations are present
|
|
relations := make(map[string]bool)
|
|
for _, tuple := range req.Deletes.TupleKeys {
|
|
relations[tuple.Relation] = true
|
|
}
|
|
require.True(t, relations["get"], "Expected 'get' relation")
|
|
require.True(t, relations["update"], "Expected 'update' relation")
|
|
require.True(t, relations["delete"], "Expected 'delete' relation")
|
|
|
|
return nil
|
|
}
|
|
|
|
b.zClient = &FakeZanzanaClient{writeCallback: testMultiDeletes}
|
|
b.AfterRoleDelete(&role, nil)
|
|
wg.Wait()
|
|
})
|
|
}
|