From 7e5cb7d8d63175a2720067be1fd8a4abe8c4b489 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 7 Jan 2025 13:49:55 +0100 Subject: [PATCH] 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 --- .../accesscontrol/dualwrite/collectors.go | 51 ++++++++ .../dualwrite/global_reconciler.go | 112 ++++++++++++++++++ .../accesscontrol/dualwrite/reconciler.go | 24 +++- pkg/services/authz/zanzana/common/tuple.go | 10 ++ pkg/services/authz/zanzana/server/server.go | 70 +++++++++++ .../authz/zanzana/server/server_check.go | 17 ++- .../authz/zanzana/server/server_list.go | 5 + .../authz/zanzana/server/server_read.go | 19 +-- pkg/services/authz/zanzana/zanzana.go | 2 + 9 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 pkg/services/accesscontrol/dualwrite/global_reconciler.go diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index d7ea5a1343c..095ee20790f 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -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) { diff --git a/pkg/services/accesscontrol/dualwrite/global_reconciler.go b/pkg/services/accesscontrol/dualwrite/global_reconciler.go new file mode 100644 index 00000000000..04b381cffa6 --- /dev/null +++ b/pkg/services/accesscontrol/dualwrite/global_reconciler.go @@ -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 +} diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index 0aa51d95592..88461ed17bf 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -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) } diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index bc4839c0128..59b20741fb9 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -82,6 +82,8 @@ var RelationsFolder = append( RelationDelete, ) +const ClusterNamespace = "cluster" + func IsGroupResourceRelation(relation string) bool { return isValidRelation(relation, RelationsGroupResource) } @@ -259,6 +261,14 @@ func ToOpenFGATupleKey(t *authzextv1.TupleKey) *openfgav1.TupleKey { return tupleKey } +func ToOpenFGATupleKeys(tuples []*authzextv1.TupleKey) []*openfgav1.TupleKey { + result := make([]*openfgav1.TupleKey, 0, len(tuples)) + for _, t := range tuples { + result = append(result, ToOpenFGATupleKey(t)) + } + return result +} + func ToOpenFGATupleKeyWithoutCondition(t *authzextv1.TupleKeyWithoutCondition) *openfgav1.TupleKeyWithoutCondition { return &openfgav1.TupleKeyWithoutCondition{ User: t.GetUser(), diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index fb1c527e294..947a06b5376 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -1,6 +1,7 @@ package server import ( + "context" "sync" "time" @@ -13,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" "github.com/grafana/grafana/pkg/setting" ) @@ -91,3 +93,71 @@ func NewAuthz(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer, opts ... return s, nil } + +func (s *Server) getGlobalAuthorizationContext(ctx context.Context) ([]*openfgav1.TupleKey, error) { + cacheKey := "global_authorization_context" + contextualTuples := make([]*openfgav1.TupleKey, 0) + + cached, found := s.cache.Get(cacheKey) + if found { + contextualTuples = cached.([]*openfgav1.TupleKey) + return contextualTuples, nil + } + + res, err := s.Read(ctx, &authzextv1.ReadRequest{ + Namespace: common.ClusterNamespace, + }) + if err != nil { + return nil, err + } + + tuples := common.ToOpenFGATuples(res.Tuples) + for _, t := range tuples { + contextualTuples = append(contextualTuples, t.GetKey()) + } + s.cache.SetDefault(cacheKey, contextualTuples) + + return contextualTuples, nil +} + +func (s *Server) addCheckAuthorizationContext(ctx context.Context, req *openfgav1.CheckRequest) error { + contextualTuples, err := s.getGlobalAuthorizationContext(ctx) + if err != nil { + return err + } + + if len(contextualTuples) == 0 { + return nil + } + + if req.ContextualTuples == nil { + req.ContextualTuples = &openfgav1.ContextualTupleKeys{} + } + if req.ContextualTuples.TupleKeys == nil { + req.ContextualTuples.TupleKeys = make([]*openfgav1.TupleKey, 0) + } + + req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, contextualTuples...) + return nil +} + +func (s *Server) addListAuthorizationContext(ctx context.Context, req *openfgav1.ListObjectsRequest) error { + contextualTuples, err := s.getGlobalAuthorizationContext(ctx) + if err != nil { + return err + } + + if len(contextualTuples) == 0 { + return nil + } + + if req.ContextualTuples == nil { + req.ContextualTuples = &openfgav1.ContextualTupleKeys{} + } + if req.ContextualTuples.TupleKeys == nil { + req.ContextualTuples.TupleKeys = make([]*openfgav1.TupleKey, 0) + } + + req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, contextualTuples...) + return nil +} diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index f7e2f489a1b..a59d8f9ef29 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -57,7 +57,7 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation, grou common.AddRenderContext(req) } - res, err := s.openfga.Check(ctx, req) + res, err := s.check(ctx, req) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, } // Check if subject has direct access to resource - res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ + res, err := s.check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, TupleKey: &openfgav1.CheckRequestTupleKey{ @@ -103,7 +103,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res if folder != "" && common.IsFolderResourceRelation(folderRelation) { // Check if subject has access as a sub resource for the folder - res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ + res, err := s.check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, TupleKey: &openfgav1.CheckRequestTupleKey{ @@ -128,7 +128,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res } // Check if subject has direct access to resource - res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ + res, err := s.check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, TupleKey: &openfgav1.CheckRequestTupleKey{ @@ -145,3 +145,12 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil } + +func (s *Server) check(ctx context.Context, req *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) { + err := s.addCheckAuthorizationContext(ctx, req) + if err != nil { + s.logger.Error("failed to add authorization context", "error", err) + } + + return s.openfga.Check(ctx, req) +} diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 18d8c6b27e0..399cb2bef32 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -39,6 +39,11 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis } func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) { + err := s.addListAuthorizationContext(ctx, req) + if err != nil { + s.logger.Error("failed to add authorization context", "error", err) + } + if s.cfg.UseStreamedListObjects { return s.streamedListObjects(ctx, req) } diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index 83fec6f15f7..1fc8100d2c9 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -18,16 +18,21 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze return nil, err } - res, err := s.openfga.Read(ctx, &openfgav1.ReadRequest{ - StoreId: storeInf.ID, - TupleKey: &openfgav1.ReadRequestTupleKey{ + readReq := &openfgav1.ReadRequest{ + StoreId: storeInf.ID, + PageSize: req.GetPageSize(), + ContinuationToken: req.GetContinuationToken(), + } + + if req.TupleKey != nil { + readReq.TupleKey = &openfgav1.ReadRequestTupleKey{ User: req.GetTupleKey().GetUser(), Relation: req.GetTupleKey().GetRelation(), Object: req.GetTupleKey().GetObject(), - }, - PageSize: req.GetPageSize(), - ContinuationToken: req.GetContinuationToken(), - }) + } + } + + res, err := s.openfga.Read(ctx, readReq) if err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index cf7d74ddaf3..110832d347e 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -58,6 +58,8 @@ const ( KindFolders string = "folders" ) +var ClusterNamespace = common.ClusterNamespace + var ( ToAuthzExtTupleKey = common.ToAuthzExtTupleKey ToAuthzExtTupleKeys = common.ToAuthzExtTupleKeys