Access: Add AfterCreate hooks for Roles/Core Roles (#112666)
As part of migrating Grafana's authorization system to Zanzana (OpenFGA), we need to ensure that role permissions defined in the IAM API are automatically synced to the authorization backend. Without this sync, roles created through the API would not be enforced by Zanzana, creating an inconsistency between defined permissions and actual authorization decisions.
This is a critical piece of the dual-write pattern during the migration to Zanzana, ensuring that:
Role permissions are immediately available for authorization checks
The legacy RBAC system and new Zanzana system remain in sync
Users experience consistent permission enforcement regardless of which backend is queried
safe to revert
This commit is contained in:
@@ -12,8 +12,10 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -100,17 +102,17 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
|
||||
return
|
||||
}
|
||||
|
||||
rp, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Grab a ticket to write to Zanzana
|
||||
// This limits the amount of concurrent writes to Zanzana
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds()) // Record wait time
|
||||
|
||||
rp, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
go func(rp *iamv0.ResourcePermission) {
|
||||
defer func() {
|
||||
// Release the ticket after write is done
|
||||
@@ -168,3 +170,127 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
|
||||
}
|
||||
}(rp.DeepCopy()) // Pass a copy of the object
|
||||
}
|
||||
|
||||
// convertRolePermissionsToTuples converts role permissions (action/scope) to v1 TupleKey format
|
||||
// using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys
|
||||
func convertRolePermissionsToTuples(roleUID string, permissions []iamv0.CoreRolespecPermission) ([]*v1.TupleKey, error) {
|
||||
// Convert IAM permissions to zanzana.RolePermission format
|
||||
rolePerms := make([]zanzana.RolePermission, 0, len(permissions))
|
||||
for _, perm := range permissions {
|
||||
// Split the scope to get kind, attribute, identifier
|
||||
kind, _, identifier := accesscontrol.SplitScope(perm.Scope)
|
||||
rolePerms = append(rolePerms, zanzana.RolePermission{
|
||||
Action: perm.Action,
|
||||
Kind: kind,
|
||||
Identifier: identifier,
|
||||
})
|
||||
}
|
||||
|
||||
// Translate to Zanzana tuples
|
||||
openfgaTuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, rolePerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert directly to v1 tuples using common utility
|
||||
v1Tuples := common.ToAuthzExtTupleKeys(openfgaTuples)
|
||||
|
||||
return v1Tuples, nil
|
||||
}
|
||||
|
||||
// AfterRoleCreate is a post-create hook that writes the role permissions to Zanzana (openFGA)
|
||||
// It handles both Role and CoreRole types
|
||||
func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, _ *metav1.CreateOptions) {
|
||||
if b.zClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract permissions based on the object type
|
||||
var roleUID, namespace string
|
||||
var permissions []iamv0.CoreRolespecPermission
|
||||
var roleType string
|
||||
|
||||
// Try CoreRole first
|
||||
if coreRole, ok := obj.(*iamv0.CoreRole); ok {
|
||||
roleUID = coreRole.Name
|
||||
namespace = coreRole.Namespace
|
||||
// Deep copy permissions to avoid race conditions
|
||||
permissions = make([]iamv0.CoreRolespecPermission, len(coreRole.Spec.Permissions))
|
||||
copy(permissions, coreRole.Spec.Permissions)
|
||||
roleType = "core role"
|
||||
} else if role, ok := obj.(*iamv0.Role); ok {
|
||||
// Try Role
|
||||
roleUID = role.Name
|
||||
namespace = role.Namespace
|
||||
|
||||
// Convert and copy permissions to avoid race conditions
|
||||
permissions = make([]iamv0.CoreRolespecPermission, len(role.Spec.Permissions))
|
||||
for i, p := range role.Spec.Permissions {
|
||||
permissions[i] = iamv0.CoreRolespecPermission(p)
|
||||
}
|
||||
roleType = "role"
|
||||
} else {
|
||||
// Not a supported role type
|
||||
return
|
||||
}
|
||||
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
<-b.zTickets
|
||||
}()
|
||||
|
||||
tuples, err := convertRolePermissionsToTuples(roleUID, permissions)
|
||||
if err != nil {
|
||||
b.logger.Error("failed to convert role permissions to tuples",
|
||||
"namespace", namespace,
|
||||
"roleUID", roleUID,
|
||||
"roleType", roleType,
|
||||
"err", err,
|
||||
"permissionsCnt", len(permissions),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid writing if there are no valid tuples
|
||||
if len(tuples) == 0 {
|
||||
b.logger.Debug("no valid tuples to write for role",
|
||||
"namespace", namespace,
|
||||
"roleUID", roleUID,
|
||||
"roleType", roleType,
|
||||
"permissionsCnt", len(permissions),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
b.logger.Debug("writing role permissions to zanzana",
|
||||
"namespace", namespace,
|
||||
"roleUID", roleUID,
|
||||
"roleType", roleType,
|
||||
"tuplesCnt", len(tuples),
|
||||
"permissionsCnt", len(permissions),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = b.zClient.Write(ctx, &v1.WriteRequest{
|
||||
Namespace: namespace,
|
||||
Writes: &v1.WriteRequestWrites{
|
||||
TupleKeys: tuples,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
b.logger.Error("failed to write role permissions to zanzana",
|
||||
"err", err,
|
||||
"namespace", namespace,
|
||||
"roleUID", roleUID,
|
||||
"roleType", roleType,
|
||||
"tuplesCnt", len(tuples),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -23,14 +23,34 @@ func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) err
|
||||
return f.writeCallback(ctx, req)
|
||||
}
|
||||
|
||||
func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
t.Skip("Need to fix its flaky behavior in CI")
|
||||
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
func requireTuplesMatch(t *testing.T, actual []*v1.TupleKey, expected []*v1.TupleKey, msgAndArgs ...interface{}) {
|
||||
t.Helper()
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if act.User == exp.User &&
|
||||
act.Relation == exp.Relation &&
|
||||
act.Object == exp.Object {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
require.Fail(t, "Expected tuple not found", "Tuple: %+v\n%v", exp, msgAndArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
t.Run("should create zanzana entries for folder resource permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
folderPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-2",
|
||||
@@ -51,16 +71,13 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 2)
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Writes.TupleKeys[0],
|
||||
&v1.TupleKey{User: "user:u1", Relation: "view", Object: "folder:fold1"},
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Writes.TupleKeys[1],
|
||||
&v1.TupleKey{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"},
|
||||
)
|
||||
|
||||
expectedTuples := []*v1.TupleKey{
|
||||
{User: "user:u1", Relation: "view", Object: "folder:fold1"},
|
||||
{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"},
|
||||
}
|
||||
|
||||
requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -68,10 +85,15 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
b.AfterResourcePermissionCreate(&folderPerm, nil)
|
||||
})
|
||||
|
||||
// Wait for the ticket to be released
|
||||
<-b.zTickets
|
||||
|
||||
t.Run("should create zanzana entries for dashboard resource permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
dashPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -95,26 +117,18 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
require.Len(t, req.Writes.TupleKeys, 2)
|
||||
require.Equal(t, "default", req.Namespace)
|
||||
|
||||
tuple1 := req.Writes.TupleKeys[0]
|
||||
require.NotNil(t, tuple1.Condition)
|
||||
require.Equal(t, "group_filter", tuple1.Condition.Name)
|
||||
tuple1.Condition = nil
|
||||
require.Equal(
|
||||
t,
|
||||
tuple1,
|
||||
&v1.TupleKey{User: "service-account:sa1", Relation: "view", Object: object},
|
||||
)
|
||||
// Verify all tuples have the group_filter condition
|
||||
for _, tuple := range req.Writes.TupleKeys {
|
||||
require.NotNil(t, tuple.Condition, "Condition should not be nil for tuple %+v", tuple)
|
||||
require.Equal(t, "group_filter", tuple.Condition.Name)
|
||||
}
|
||||
|
||||
tuple2 := req.Writes.TupleKeys[1]
|
||||
require.NotNil(t, tuple2.Condition)
|
||||
require.Equal(t, "group_filter", tuple2.Condition.Name)
|
||||
tuple2.Condition = nil
|
||||
require.Equal(
|
||||
t,
|
||||
tuple2,
|
||||
&v1.TupleKey{User: "team:team1", Relation: "edit", Object: object},
|
||||
)
|
||||
expectedTuples := []*v1.TupleKey{
|
||||
{User: "service-account:sa1", Relation: "view", Object: object},
|
||||
{User: "team:team1", Relation: "edit", Object: object},
|
||||
}
|
||||
|
||||
requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,3 +136,300 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
b.AfterResourcePermissionCreate(&dashPerm, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAfterCoreRoleCreate(t *testing.T) {
|
||||
t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("should create zanzana entries for core role with dashboard permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("should handle wildcard scopes", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("should skip untranslatable permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAfterRoleCreate(t *testing.T) {
|
||||
t.Run("should create zanzana entries for role with folder permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("should create zanzana entries for role with dashboard permissions", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("should merge folder resource tuples with same object and user", func(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
zTickets: make(chan bool, 1),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
<-b.zTickets
|
||||
})
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -263,12 +263,20 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enableZanzanaSync {
|
||||
b.logger.Info("Enabling AfterCreate hook for CoreRole to sync to Zanzana")
|
||||
coreRoleStore.AfterCreate = b.AfterRoleCreate
|
||||
}
|
||||
storage[iamv0.CoreRoleInfo.StoragePath()] = coreRoleStore
|
||||
|
||||
roleStore, err := NewLocalStore(iamv0.RoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.rolesStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enableZanzanaSync {
|
||||
b.logger.Info("Enabling AfterCreate hook for Role to sync to Zanzana")
|
||||
roleStore.AfterCreate = b.AfterRoleCreate
|
||||
}
|
||||
storage[iamv0.RoleInfo.StoragePath()] = roleStore
|
||||
|
||||
roleBindingStore, err := NewLocalStore(iamv0.RoleBindingInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.roleBindingsStorage)
|
||||
|
||||
Reference in New Issue
Block a user