Zanzana: Refactor fixed roles (use global store) (#97884)

* Zanzana: Pass contextual tuples for authorization

* global reconciler for fixed roles

* inject tuples from global store

* fix adding contextual tuples

* cleanup

* don't error on auth context fail

* add todo

* add context for List

* add caching

* remove unused

* use constant for global namespace

* Rename global namespace to cluster namespace
This commit is contained in:
Alexander Zobnin
2025-01-07 13:49:55 +01:00
committed by GitHub
parent 9558a25ebe
commit 7e5cb7d8d6
9 changed files with 298 additions and 12 deletions
@@ -359,6 +359,7 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector {
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE (r.org_id = 0 OR r.org_id = ?)
AND r.name NOT LIKE 'managed:%'
AND r.name NOT LIKE 'fixed:%'
`
type Permission struct {
@@ -400,6 +401,56 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector {
}
}
func fixedRolePermissionsCollector(store db.DB) globalTupleCollector {
return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) {
var query = `
SELECT r.uid as role_uid, p.action, p.kind, p.identifier
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE r.org_id = 0
AND r.name LIKE 'fixed:%'
`
type Permission struct {
Action string `xorm:"action"`
Kind string
Identifier string
RoleUID string `xorm:"role_uid"`
}
var permissions []Permission
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query).Find(&permissions)
})
if err != nil {
return nil, err
}
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 {
continue
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
}
}
// basicRoleBindingsCollector collects role bindings for basic roles
func anonymousRoleBindingsCollector(cfg *setting.Cfg, store db.DB) legacyTupleCollector {
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
@@ -0,0 +1,112 @@
package dualwrite
import (
"context"
"fmt"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
)
type globalTupleCollector func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error)
type globalReconciler struct {
name string
globalCollector globalTupleCollector
zanzana zanzanaTupleCollector
client zanzana.Client
}
func newGlobalReconciler(name string, globalCollector globalTupleCollector, zanzana zanzanaTupleCollector, client zanzana.Client) globalReconciler {
return globalReconciler{name, globalCollector, zanzana, client}
}
func (r globalReconciler) reconcile(ctx context.Context) error {
namespace := zanzana.ClusterNamespace
// 1. Fetch grafana resources stored in grafana db.
res, err := r.globalCollector(ctx)
if err != nil {
return fmt.Errorf("failed to collect legacy tuples for %s: %w", r.name, err)
}
var (
writes = []*openfgav1.TupleKey{}
deletes = []*openfgav1.TupleKeyWithoutCondition{}
)
for object, tuples := range res {
// 2. Fetch all tuples for given object.
// Due to limitations in open fga api we need to collect tuples per object
zanzanaTuples, err := r.zanzana(ctx, r.client, object, namespace)
if err != nil {
return fmt.Errorf("failed to collect zanzanaa tuples for %s: %w", r.name, err)
}
// 3. Check if tuples from grafana db exists in zanzana and if not add them to writes
for key, t := range tuples {
stored, ok := zanzanaTuples[key]
if !ok {
writes = append(writes, t)
continue
}
// 4. For folder resource tuples we also need to compare the stored group_resources
if zanzana.IsFolderResourceTuple(t) && t.String() != stored.String() {
deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
User: t.User,
Relation: t.Relation,
Object: t.Object,
})
writes = append(writes, t)
}
}
// 5. Check if tuple from zanzana don't exists in grafana db, if not add them to deletes.
for key, tuple := range zanzanaTuples {
_, ok := tuples[key]
if !ok {
deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
})
}
}
}
if len(writes) == 0 && len(deletes) == 0 {
return nil
}
if len(deletes) > 0 {
err := batch(deletes, 100, func(items []*openfgav1.TupleKeyWithoutCondition) error {
return r.client.Write(ctx, &authzextv1.WriteRequest{
Namespace: namespace,
Deletes: &authzextv1.WriteRequestDeletes{TupleKeys: zanzana.ToAuthzExtTupleKeysWithoutCondition(items)},
})
})
if err != nil {
return err
}
}
if len(writes) > 0 {
err := batch(writes, 100, func(items []*openfgav1.TupleKey) error {
return r.client.Write(ctx, &authzextv1.WriteRequest{
Namespace: namespace,
Writes: &authzextv1.WriteRequestWrites{TupleKeys: zanzana.ToAuthzExtTupleKeys(items)},
})
})
if err != nil {
return err
}
}
return nil
}
@@ -32,7 +32,8 @@ type ZanzanaReconciler struct {
lock *serverlock.ServerLockService
// reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store.
// These are run periodically to try to maintain a consistent state.
reconcilers []resourceReconciler
reconcilers []resourceReconciler
globalReconcilers []globalReconciler
}
func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler {
@@ -92,6 +93,14 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB,
client,
),
},
globalReconcilers: []globalReconciler{
newGlobalReconciler(
"fixed role pemissions",
fixedRolePermissionsCollector(store),
zanzanaCollector([]string{zanzana.RelationAssignee}),
client,
),
},
}
if cfg.Anonymous.Enabled {
@@ -134,8 +143,19 @@ func (r *ZanzanaReconciler) ReconcileSync(ctx context.Context) error {
}
func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
runGlobal := func(ctx context.Context) {
for _, reconciler := range r.globalReconcilers {
r.log.Debug("Performing zanzana reconciliation", "reconciler", reconciler.name)
if err := reconciler.reconcile(ctx); err != nil {
r.log.Warn("Failed to perform reconciliation for resource", "err", err)
}
}
}
run := func(ctx context.Context, namespace string) {
now := time.Now()
r.log.Debug("Started reconciliation")
for _, reconciler := range r.reconcilers {
r.log.Debug("Performing zanzana reconciliation", "reconciler", reconciler.name)
if err := reconciler.reconcile(ctx, namespace); err != nil {
@@ -167,6 +187,7 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
}
if r.lock == nil {
runGlobal(ctx)
for _, ns := range namespaces {
run(ctx, ns)
}
@@ -175,6 +196,7 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
// We ignore the error for now
err := r.lock.LockExecuteAndRelease(ctx, "zanzana-reconciliation", 10*time.Hour, func(ctx context.Context) {
runGlobal(ctx)
for _, ns := range namespaces {
run(ctx, ns)
}