Files
grafana/pkg/services/authz/zanzana/tuple_helpers.go
Jo 2e1704b56f 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
2025-10-23 09:47:39 +02:00

82 lines
2.7 KiB
Go

package zanzana
import (
"github.com/grafana/grafana/pkg/infra/log"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
// TupleStringWithoutCondition returns the string representation of a tuple without its condition.
// This is useful for deduplicating tuples that have the same user, relation, and object
// but different conditions that need to be merged.
func TupleStringWithoutCondition(tuple *openfgav1.TupleKey) string {
c := tuple.Condition
tuple.Condition = nil
s := tuple.String()
tuple.Condition = c
return s
}
// RolePermission represents a permission that can be converted to a Zanzana tuple.
type RolePermission struct {
Action string
Kind string
Identifier string
}
// ConvertRolePermissionsToTuples converts role permissions to Zanzana tuples with proper merging.
// It handles:
// - Translation of RBAC action/kind/identifier to Zanzana tuples
// - Special handling for folder resource tuples (which need to be merged)
// - Deduplication of tuples
//
// Returns a slice of tuples ready to be written to Zanzana, or nil if no valid tuples could be created.
func ConvertRolePermissionsToTuples(roleUID string, permissions []RolePermission) ([]*openfgav1.TupleKey, error) {
if len(permissions) == 0 {
return nil, nil
}
// Subject for role permissions: role:{uid}#assignee
subject := NewTupleEntry(TypeRole, roleUID, RelationAssignee)
// Use a map to track tuples, with special handling for folder resource tuples
tupleMap := make(map[string]*openfgav1.TupleKey)
folderResourceTuples := make(map[string]*openfgav1.TupleKey) // key is tuple without condition
for _, perm := range permissions {
// Convert RBAC action/kind to Zanzana tuple
tuple, ok := TranslateToResourceTuple(subject, perm.Action, perm.Kind, perm.Identifier)
if !ok {
// Skip permissions that can't be translated
log.New("zanzana").Debug("skipping permission that can't be translated", "permission", perm)
continue
}
// Handle folder resource tuples specially - they need to be merged
if IsFolderResourceTuple(tuple) {
// Create a key without the condition for deduplication
key := TupleStringWithoutCondition(tuple)
if existing, exists := folderResourceTuples[key]; exists {
// Merge this tuple with the existing one
MergeFolderResourceTuples(existing, tuple)
} else {
folderResourceTuples[key] = tuple
}
continue
}
// For non-folder resource tuples, just add to the map
tupleMap[tuple.String()] = tuple
}
// Collect all tuples
tuples := make([]*openfgav1.TupleKey, 0, len(tupleMap)+len(folderResourceTuples))
for _, t := range tupleMap {
tuples = append(tuples, t)
}
for _, t := range folderResourceTuples {
tuples = append(tuples, t)
}
return tuples, nil
}