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:
Jo
2025-10-23 09:47:39 +02:00
committed by GitHub
parent cd961af818
commit 2e1704b56f
7 changed files with 1816 additions and 74 deletions
@@ -185,7 +185,7 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector
// For resource actions on folders we need to merge the tuples into one with combined subresources.
if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple)
key := zanzana.TupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok {
zanzana.MergeFolderResourceTuples(t, tuple)
} else {
@@ -202,14 +202,6 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector
}
}
func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string {
c := tuple.Condition
tuple.Condition = nil
s := tuple.String()
tuple.Condition = c
return s
}
// basicRoleBindingsCollector collects role bindings for basic roles
func basicRoleBindingsCollector(store db.DB) legacyTupleCollector {
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
@@ -371,36 +363,41 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector {
return nil, err
}
// Group permissions by role UID
rolePermissionsMap := make(map[string][]zanzana.RolePermission)
for _, p := range permissions {
rolePermissionsMap[p.RoleUID] = append(rolePermissionsMap[p.RoleUID], zanzana.RolePermission{
Action: p.Action,
Kind: p.Kind,
Identifier: p.Identifier,
})
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, p := range permissions {
tuple, ok := zanzana.TranslateToResourceTuple(
zanzana.NewTupleEntry(zanzana.TypeRole, p.RoleUID, zanzana.RelationAssignee),
p.Action,
p.Kind,
p.Identifier,
)
if !ok {
// Convert permissions for each role using the shared utility
for roleUID, perms := range rolePermissionsMap {
roleTuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, perms)
if err != nil {
reconcilerLogger.Warn("Failed to convert role permissions to tuples", "roleUID", roleUID, "err", err)
continue
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
// For resource actions on folders we need to merge the tuples into one with combined subresources.
if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok {
zanzana.MergeFolderResourceTuples(t, tuple)
} else {
tuples[tuple.Object][key] = tuple
// Add tuples to the result map
for _, tuple := range roleTuples {
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
continue
// Use the appropriate key based on whether it's a folder resource tuple
var key string
if zanzana.IsFolderResourceTuple(tuple) {
key = zanzana.TupleStringWithoutCondition(tuple)
} else {
key = tuple.String()
}
tuples[tuple.Object][key] = tuple
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
@@ -482,7 +479,7 @@ func zanzanaCollector(relations []string) zanzanaTupleCollector {
}
for _, t := range tuples {
if zanzana.IsFolderResourceTuple(t.Key) {
out[tupleStringWithoutCondition(t.Key)] = t.Key
out[zanzana.TupleStringWithoutCondition(t.Key)] = t.Key
} else {
out[t.Key.String()] = t.Key
}
File diff suppressed because it is too large Load Diff