From 910ec7e7dc29fbc15b26ae366a5b842c12d98aed Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 Nov 2024 14:54:36 +0100 Subject: [PATCH] Zanzana: Use separate store for each org (#96015) * Move server init into server package * map store name to id * refactor model loading * pass namespace into reconcilers and collectors * refactor * Extend authz server with Read and Write methods * use new read/write in reconciler * implement server side read and write * Sync permissions for every org * handle namespace in check and list * split read and write * provide conditions * Fix client implementation * fix nil conditions * remove unused client code * use lock for store access * move type translators to common package * fix folder collector * fix store creation * remove unused AuthorizationModelId * fix server tests * fix linter --- .../accesscontrol/dualwrite/collectors.go | 29 +- .../accesscontrol/dualwrite/reconciler.go | 48 +- .../dualwrite/resource_reconciler.go | 28 +- pkg/services/authz/zanzana.go | 19 +- pkg/services/authz/zanzana/client.go | 24 +- pkg/services/authz/zanzana/client/client.go | 111 +-- pkg/services/authz/zanzana/client/noop.go | 5 +- pkg/services/authz/zanzana/common/tuple.go | 83 ++ .../authz/zanzana/proto/v1/extention.pb.go | 808 +++++++++++++++++- .../authz/zanzana/proto/v1/extention.proto | 65 ++ .../zanzana/proto/v1/extention_grpc.pb.go | 78 +- pkg/services/authz/zanzana/server.go | 34 - pkg/services/authz/zanzana/server/server.go | 159 +--- .../authz/zanzana/server/server_check.go | 33 +- .../authz/zanzana/server/server_check_test.go | 15 +- .../authz/zanzana/server/server_list.go | 31 +- .../authz/zanzana/server/server_list_test.go | 11 +- .../authz/zanzana/server/server_read.go | 47 + .../authz/zanzana/server/server_store.go | 199 +++++ .../authz/zanzana/server/server_test.go | 8 +- .../authz/zanzana/server/server_write.go | 55 ++ pkg/services/authz/zanzana/zanzana.go | 3 +- 22 files changed, 1497 insertions(+), 396 deletions(-) create mode 100644 pkg/services/authz/zanzana/server/server_read.go create mode 100644 pkg/services/authz/zanzana/server/server_store.go create mode 100644 pkg/services/authz/zanzana/server/server_write.go diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index 58f339d22df..65dbc6da528 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -3,13 +3,16 @@ package dualwrite import ( "context" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/authz/zanzana" - openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) func teamMembershipCollector(store db.DB) legacyTupleCollector { - return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { + return func(ctx context.Context, orgId int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT t.uid as team_uid, u.uid as user_uid, tm.permission FROM team_member tm @@ -60,7 +63,7 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector { // folderTreeCollector collects folder tree structure and writes it as relation tuples func folderTreeCollector(store db.DB) legacyTupleCollector { - return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { + return func(ctx context.Context, orgId int64) (map[string]map[string]*openfgav1.TupleKey, error) { ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector") defer span.End() @@ -91,9 +94,9 @@ func folderTreeCollector(store db.DB) legacyTupleCollector { } tuple = &openfgav1.TupleKey{ - Object: zanzana.NewTupleEntry("folder2", f.FolderUID, ""), + Object: zanzana.NewTupleEntry(common.TypeFolder, f.FolderUID, ""), Relation: zanzana.RelationParent, - User: zanzana.NewTupleEntry("folder2", f.ParentUID, ""), + User: zanzana.NewTupleEntry(common.TypeFolder, f.ParentUID, ""), } if tuples[tuple.Object] == nil { @@ -111,7 +114,7 @@ func folderTreeCollector(store db.DB) legacyTupleCollector { // It will only store actions that are supported by our schema. Managed permissions can // be directly mapped to user/team/role without having to write an intermediate role. func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector { - return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { + return func(ctx context.Context, orgId int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id FROM permission p @@ -194,11 +197,12 @@ func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string { } func zanzanaCollector(relations []string) zanzanaTupleCollector { - return func(ctx context.Context, client zanzana.Client, object string) (map[string]*openfgav1.TupleKey, error) { + return func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error) { // list will use continuation token to collect all tuples for object and relation list := func(relation string) ([]*openfgav1.Tuple, error) { - first, err := client.Read(ctx, &openfgav1.ReadRequest{ - TupleKey: &openfgav1.ReadRequestTupleKey{ + first, err := client.Read(ctx, &authzextv1.ReadRequest{ + Namespace: namespace, + TupleKey: &authzextv1.ReadRequestTupleKey{ Object: object, Relation: relation, }, @@ -211,8 +215,9 @@ func zanzanaCollector(relations []string) zanzanaTupleCollector { c := first.ContinuationToken for c != "" { - res, err := client.Read(ctx, &openfgav1.ReadRequest{ - TupleKey: &openfgav1.ReadRequestTupleKey{ + res, err := client.Read(ctx, &authzextv1.ReadRequest{ + Namespace: namespace, + TupleKey: &authzextv1.ReadRequestTupleKey{ Object: object, Relation: relation, }, @@ -225,7 +230,7 @@ func zanzanaCollector(relations []string) zanzanaTupleCollector { first.Tuples = append(first.Tuples, res.Tuples...) } - return first.Tuples, nil + return common.ToOpenFGATuples(first.Tuples), nil } out := make(map[string]*openfgav1.TupleKey) diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index b1894c1eea6..88d7221457e 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/grafana/authlib/claims" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel" "github.com/grafana/grafana/pkg/infra/db" @@ -12,7 +14,11 @@ import ( "github.com/grafana/grafana/pkg/services/authz/zanzana" ) -var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/reconciler") +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/migrator") + +// A TupleCollector is responsible to build and store [openfgav1.TupleKey] into provided tuple map. +// They key used should be a unique group key for the collector so we can skip over an already synced group. +type TupleCollector func(ctx context.Context, namespace string, tuples map[string][]*openfgav1.TupleKey) error // ZanzanaReconciler is a component to reconcile RBAC permissions to zanzana. // We should rewrite the migration after we have "migrated" all possible actions @@ -20,6 +26,7 @@ var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/reconcile type ZanzanaReconciler struct { lock *serverlock.ServerLockService log log.Logger + store db.DB client zanzana.Client // 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. @@ -31,6 +38,7 @@ func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.S client: client, lock: lock, log: log.New("zanzana.reconciler"), + store: store, reconcilers: []resourceReconciler{ newResourceReconciler( "team memberships", @@ -93,23 +101,47 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { } func (r *ZanzanaReconciler) reconcile(ctx context.Context) { - run := func(ctx context.Context) { + run := func(ctx context.Context, namespace string) { now := time.Now() for _, reconciler := range r.reconcilers { - if err := reconciler.reconcile(ctx); err != nil { + if err := reconciler.reconcile(ctx, namespace); err != nil { r.log.Warn("Failed to perform reconciliation for resource", "err", err) } } r.log.Debug("Finished reconciliation", "elapsed", time.Since(now)) } - if r.lock == nil { - run(ctx) + orgIds, err := r.getOrgs(ctx) + if err != nil { return } - // We ignore the error for now - _ = r.lock.LockExecuteAndRelease(ctx, "zanzana-reconciliation", 10*time.Hour, func(ctx context.Context) { - run(ctx) + for _, orgId := range orgIds { + ns := claims.OrgNamespaceFormatter(orgId) + + if r.lock == nil { + run(ctx, ns) + return + } + + // We ignore the error for now + _ = r.lock.LockExecuteAndRelease(ctx, "zanzana-reconciliation", 10*time.Hour, func(ctx context.Context) { + run(ctx, ns) + }) + } +} + +func (r *ZanzanaReconciler) getOrgs(ctx context.Context) ([]int64, error) { + orgs := make([]int64, 0) + err := r.store.WithDbSession(ctx, func(sess *db.Session) error { + q := "SELECT id FROM org" + if err := sess.SQL(q).Find(&orgs); err != nil { + return err + } + return nil }) + if err != nil { + return nil, err + } + return orgs, nil } diff --git a/pkg/services/accesscontrol/dualwrite/resource_reconciler.go b/pkg/services/accesscontrol/dualwrite/resource_reconciler.go index 32cc1160225..c59115b4ff3 100644 --- a/pkg/services/accesscontrol/dualwrite/resource_reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/resource_reconciler.go @@ -4,16 +4,19 @@ import ( "context" "fmt" + "github.com/grafana/authlib/claims" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) // legacyTupleCollector collects tuples groupd by object and tupleKey -type legacyTupleCollector func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) +type legacyTupleCollector func(ctx context.Context, orgId int64) (map[string]map[string]*openfgav1.TupleKey, error) // zanzanaTupleCollector collects tuples from zanzana for given object -type zanzanaTupleCollector func(ctx context.Context, client zanzana.Client, object string) (map[string]*openfgav1.TupleKey, error) +type zanzanaTupleCollector func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error) type resourceReconciler struct { name string @@ -26,9 +29,14 @@ func newResourceReconciler(name string, legacy legacyTupleCollector, zanzana zan return resourceReconciler{name, legacy, zanzana, client} } -func (r resourceReconciler) reconcile(ctx context.Context) error { +func (r resourceReconciler) reconcile(ctx context.Context, namespace string) error { + info, err := claims.ParseNamespace(namespace) + if err != nil { + return err + } + // 1. Fetch grafana resources stored in grafana db. - res, err := r.legacy(ctx) + res, err := r.legacy(ctx, info.OrgID) if err != nil { return fmt.Errorf("failed to collect legacy tuples for %s: %w", r.name, err) } @@ -41,7 +49,7 @@ func (r resourceReconciler) reconcile(ctx context.Context) error { 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) + 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) } @@ -85,8 +93,9 @@ func (r resourceReconciler) reconcile(ctx context.Context) error { if len(deletes) > 0 { err := batch(deletes, 100, func(items []*openfgav1.TupleKeyWithoutCondition) error { - return r.client.Write(ctx, &openfgav1.WriteRequest{ - Deletes: &openfgav1.WriteRequestDeletes{TupleKeys: items}, + return r.client.Write(ctx, &authzextv1.WriteRequest{ + Namespace: namespace, + Deletes: &authzextv1.WriteRequestDeletes{TupleKeys: common.ToAuthzExtTupleKeysWithoutCondition(items)}, }) }) @@ -97,8 +106,9 @@ func (r resourceReconciler) reconcile(ctx context.Context) error { if len(writes) > 0 { err := batch(writes, 100, func(items []*openfgav1.TupleKey) error { - return r.client.Write(ctx, &openfgav1.WriteRequest{ - Writes: &openfgav1.WriteRequestWrites{TupleKeys: items}, + return r.client.Write(ctx, &authzextv1.WriteRequest{ + Namespace: namespace, + Writes: &authzextv1.WriteRequestWrites{TupleKeys: common.ToAuthzExtTupleKeys(items)}, }) }) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 4b997ae6a6d..7599d900408 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -17,8 +17,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authz/zanzana" - "github.com/grafana/grafana/pkg/services/authz/zanzana/client" + zclient "github.com/grafana/grafana/pkg/services/authz/zanzana/client" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" + zserver "github.com/grafana/grafana/pkg/services/authz/zanzana/server" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/setting" @@ -28,7 +29,7 @@ import ( // It will also start an embedded ZanzanaSever if mode is set to "embedded". func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) { if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { - return client.NewNoop(), nil + return zclient.NewNoop(), nil } logger := log.New("zanzana") @@ -41,7 +42,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } - client, err = zanzana.NewClient(context.Background(), conn, cfg) + client, err = zclient.NewClient(context.Background(), conn, cfg) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } @@ -51,12 +52,12 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg return nil, fmt.Errorf("failed to start zanzana: %w", err) } - openfga, err := zanzana.NewOpenFGAServer(cfg, store, logger) + openfga, err := zserver.NewOpenFGA(&cfg.Zanzana, store, logger) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewAuthzServer(cfg, openfga) + srv, err := zserver.NewAuthzServer(cfg, openfga) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -65,7 +66,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg authzv1.RegisterAuthzServiceServer(channel, srv) authzextv1.RegisterAuthzExtentionServiceServer(channel, srv) - client, err = zanzana.NewClient(context.Background(), channel, cfg) + client, err = zclient.NewClient(context.Background(), channel, cfg) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } @@ -112,12 +113,12 @@ func (z *Zanzana) start(ctx context.Context) error { return fmt.Errorf("failed to initilize zanana store: %w", err) } - openfga, err := zanzana.NewOpenFGAServer(z.cfg, store, z.logger) + openfga, err := zserver.NewOpenFGA(&z.cfg.Zanzana, store, z.logger) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewAuthzServer(z.cfg, openfga) + srv, err := zserver.NewAuthzServer(z.cfg, openfga) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } @@ -156,7 +157,7 @@ func (z *Zanzana) running(ctx context.Context) error { if z.cfg.Env == setting.Dev && z.cfg.Zanzana.ListenHTTP { go func() { z.logger.Info("Starting OpenFGA HTTP server") - err := zanzana.StartOpenFGAHttpSever(z.cfg, z.handle, z.logger) + err := zserver.StartOpenFGAHttpSever(z.cfg, z.handle, z.logger) if err != nil { z.logger.Error("failed to start OpenFGA HTTP server", "error", err) } diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index d1f43f6e39f..facfdf00c1d 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -2,40 +2,20 @@ package zanzana import ( "context" - "fmt" "github.com/grafana/authlib/authz" "github.com/grafana/authlib/claims" - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "google.golang.org/grpc" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authz/zanzana/client" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" - "github.com/grafana/grafana/pkg/setting" ) // Client is a wrapper around [openfgav1.OpenFGAServiceClient] type Client interface { authz.AccessClient List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) - - Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) - Write(ctx context.Context, in *openfgav1.WriteRequest) error -} - -func NewClient(ctx context.Context, cc grpc.ClientConnInterface, cfg *setting.Cfg) (*client.Client, error) { - stackID := cfg.StackID - if stackID == "" { - stackID = "default" - } - - return client.New( - ctx, - cc, - client.WithTenantID(fmt.Sprintf("stacks-%s", stackID)), - client.WithLogger(log.New("zanzana-client")), - ) + Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) + Write(ctx context.Context, req *authzextv1.WriteRequest) error } func NewNoopClient() *client.NoopClient { diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 158c987fce7..9a519345509 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -2,21 +2,19 @@ package client import ( "context" - "errors" "fmt" - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "go.opentelemetry.io/otel" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/grafana/authlib/authz" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/claims" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel" + "google.golang.org/grpc" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" + "github.com/grafana/grafana/pkg/setting" ) var _ authz.AccessClient = (*Client)(nil) @@ -42,10 +40,21 @@ type Client struct { openfga openfgav1.OpenFGAServiceClient authz authzv1.AuthzServiceClient authzext authzextv1.AuthzExtentionServiceClient - tenantID string - storeID string - modelID string +} + +func NewClient(ctx context.Context, cc grpc.ClientConnInterface, cfg *setting.Cfg) (*Client, error) { + stackID := cfg.StackID + if stackID == "" { + stackID = "default" + } + + return New( + ctx, + cc, + WithTenantID(fmt.Sprintf("stacks-%s", stackID)), + WithLogger(log.New("zanzana-client")), + ) } func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) (*Client, error) { @@ -63,24 +72,6 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) c.logger = log.NewNopLogger() } - if c.tenantID == "" { - c.tenantID = "stacks-default" - } - - store, err := c.getStore(ctx, c.tenantID) - if err != nil { - return nil, err - } - - c.storeID = store.GetId() - - modelID, err := c.loadModel(ctx, c.storeID) - if err != nil { - return nil, err - } - - c.modelID = modelID - return c, nil } @@ -141,69 +132,17 @@ func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListReq }) } -func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) { +func (c *Client) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) { ctx, span := tracer.Start(ctx, "authz.zanzana.client.Read") defer span.End() - in.StoreId = c.storeID - return c.openfga.Read(ctx, in) + return c.authzext.Read(ctx, req) } -func (c *Client) Write(ctx context.Context, in *openfgav1.WriteRequest) error { - in.StoreId = c.storeID - in.AuthorizationModelId = c.modelID - _, err := c.openfga.Write(ctx, in) +func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error { + ctx, span := tracer.Start(ctx, "authz.zanzana.client.Write") + defer span.End() + + _, err := c.authzext.Write(ctx, req) return err } - -var errStoreNotFound = errors.New("store not found") - -func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, error) { - var continuationToken string - - // OpenFGA client does not support any filters for stores. - // We should create an issue to support some way to get stores by name. - // For now we need to go thourh all stores until we find a match or we hit the end. - for { - res, err := c.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ - PageSize: &wrapperspb.Int32Value{Value: 20}, - ContinuationToken: continuationToken, - }) - - if err != nil { - return nil, fmt.Errorf("failed to initiate zanzana tenant: %w", err) - } - - for _, s := range res.GetStores() { - if s.GetName() == name { - return s, nil - } - } - - // we have no more stores to check - if res.GetContinuationToken() == "" { - return nil, errStoreNotFound - } - - continuationToken = res.GetContinuationToken() - } -} - -func (c *Client) loadModel(ctx context.Context, storeID string) (string, error) { - // ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation. - // So with a pageSize of 1 we will get the latest model. - res, err := c.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{ - StoreId: storeID, - PageSize: &wrapperspb.Int32Value{Value: 1}, - }) - - if err != nil { - return "", fmt.Errorf("failed to load latest authorization model: %w", err) - } - - if len(res.AuthorizationModels) != 1 { - return "", fmt.Errorf("failed to load latest authorization model") - } - - return res.AuthorizationModels[0].GetId(), nil -} diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index f17b8be482d..d71968c7180 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/authlib/authz" "github.com/grafana/authlib/claims" - openfgav1 "github.com/openfga/api/proto/openfga/v1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) @@ -30,10 +29,10 @@ func (nc *NoopClient) List(ctx context.Context, id claims.AuthInfo, req authz.Li return nil, nil } -func (nc NoopClient) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) { +func (nc NoopClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) { return nil, nil } -func (nc NoopClient) Write(ctx context.Context, in *openfgav1.WriteRequest) error { +func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) error { return nil } diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 09f6c95aaa5..fba017ce2fd 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -5,6 +5,8 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) const ( @@ -98,3 +100,84 @@ func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey { Object: NewTypedIdent(typ, name), } } + +func ToAuthzExtTupleKey(t *openfgav1.TupleKey) *authzextv1.TupleKey { + tupleKey := &authzextv1.TupleKey{ + User: t.GetUser(), + Relation: t.GetRelation(), + Object: t.GetObject(), + } + + if t.GetCondition() != nil { + tupleKey.Condition = &authzextv1.RelationshipCondition{ + Name: t.GetCondition().GetName(), + Context: t.GetCondition().GetContext(), + } + } + + return tupleKey +} + +func ToAuthzExtTupleKeys(tuples []*openfgav1.TupleKey) []*authzextv1.TupleKey { + result := make([]*authzextv1.TupleKey, 0, len(tuples)) + for _, t := range tuples { + result = append(result, ToAuthzExtTupleKey(t)) + } + return result +} + +func ToAuthzExtTupleKeyWithoutCondition(t *openfgav1.TupleKeyWithoutCondition) *authzextv1.TupleKeyWithoutCondition { + return &authzextv1.TupleKeyWithoutCondition{ + User: t.GetUser(), + Relation: t.GetRelation(), + Object: t.GetObject(), + } +} + +func ToAuthzExtTupleKeysWithoutCondition(tuples []*openfgav1.TupleKeyWithoutCondition) []*authzextv1.TupleKeyWithoutCondition { + result := make([]*authzextv1.TupleKeyWithoutCondition, 0, len(tuples)) + for _, t := range tuples { + result = append(result, ToAuthzExtTupleKeyWithoutCondition(t)) + } + return result +} + +func ToOpenFGATupleKey(t *authzextv1.TupleKey) *openfgav1.TupleKey { + tupleKey := &openfgav1.TupleKey{ + User: t.GetUser(), + Relation: t.GetRelation(), + Object: t.GetObject(), + } + + if t.GetCondition() != nil { + tupleKey.Condition = &openfgav1.RelationshipCondition{ + Name: t.GetCondition().GetName(), + Context: t.GetCondition().GetContext(), + } + } + + return tupleKey +} + +func ToOpenFGATupleKeyWithoutCondition(t *authzextv1.TupleKeyWithoutCondition) *openfgav1.TupleKeyWithoutCondition { + return &openfgav1.TupleKeyWithoutCondition{ + User: t.GetUser(), + Relation: t.GetRelation(), + Object: t.GetObject(), + } +} + +func ToOpenFGATuple(t *authzextv1.Tuple) *openfgav1.Tuple { + return &openfgav1.Tuple{ + Key: ToOpenFGATupleKey(t.GetKey()), + Timestamp: t.GetTimestamp(), + } +} + +func ToOpenFGATuples(tuples []*authzextv1.Tuple) []*openfgav1.Tuple { + result := make([]*openfgav1.Tuple, 0, len(tuples)) + for _, t := range tuples { + result = append(result, ToOpenFGATuple(t)) + } + return result +} diff --git a/pkg/services/authz/zanzana/proto/v1/extention.pb.go b/pkg/services/authz/zanzana/proto/v1/extention.pb.go index 0fe8e9d841a..9e5fdc71d17 100644 --- a/pkg/services/authz/zanzana/proto/v1/extention.pb.go +++ b/pkg/services/authz/zanzana/proto/v1/extention.pb.go @@ -9,6 +9,9 @@ package v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" ) @@ -158,36 +161,740 @@ func (x *ListResponse) GetItems() []string { return nil } +type TupleKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"` + Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"` + Condition *RelationshipCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"` +} + +func (x *TupleKey) Reset() { + *x = TupleKey{} + mi := &file_extention_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TupleKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TupleKey) ProtoMessage() {} + +func (x *TupleKey) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. +func (*TupleKey) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{2} +} + +func (x *TupleKey) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *TupleKey) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *TupleKey) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *TupleKey) GetCondition() *RelationshipCondition { + if x != nil { + return x.Condition + } + return nil +} + +type Tuple struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key *TupleKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *Tuple) Reset() { + *x = Tuple{} + mi := &file_extention_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Tuple) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Tuple) ProtoMessage() {} + +func (x *Tuple) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Tuple.ProtoReflect.Descriptor instead. +func (*Tuple) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{3} +} + +func (x *Tuple) GetKey() *TupleKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *Tuple) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type TupleKeyWithoutCondition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"` + Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"` +} + +func (x *TupleKeyWithoutCondition) Reset() { + *x = TupleKeyWithoutCondition{} + mi := &file_extention_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TupleKeyWithoutCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TupleKeyWithoutCondition) ProtoMessage() {} + +func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. +func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{4} +} + +func (x *TupleKeyWithoutCondition) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *TupleKeyWithoutCondition) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *TupleKeyWithoutCondition) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +type RelationshipCondition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Context *structpb.Struct `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` +} + +func (x *RelationshipCondition) Reset() { + *x = RelationshipCondition{} + mi := &file_extention_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationshipCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationshipCondition) ProtoMessage() {} + +func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. +func (*RelationshipCondition) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{5} +} + +func (x *RelationshipCondition) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RelationshipCondition) GetContext() *structpb.Struct { + if x != nil { + return x.Context + } + return nil +} + +type ReadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Use namespace instead of store id. It will be handled on the server side + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + TupleKey *ReadRequestTupleKey `protobuf:"bytes,2,opt,name=tuple_key,json=tupleKey,proto3" json:"tuple_key,omitempty"` + PageSize *wrapperspb.Int32Value `protobuf:"bytes,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + ContinuationToken string `protobuf:"bytes,4,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"` +} + +func (x *ReadRequest) Reset() { + *x = ReadRequest{} + mi := &file_extention_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadRequest) ProtoMessage() {} + +func (x *ReadRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. +func (*ReadRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{6} +} + +func (x *ReadRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ReadRequest) GetTupleKey() *ReadRequestTupleKey { + if x != nil { + return x.TupleKey + } + return nil +} + +func (x *ReadRequest) GetPageSize() *wrapperspb.Int32Value { + if x != nil { + return x.PageSize + } + return nil +} + +func (x *ReadRequest) GetContinuationToken() string { + if x != nil { + return x.ContinuationToken + } + return "" +} + +type ReadRequestTupleKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"` + Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"` +} + +func (x *ReadRequestTupleKey) Reset() { + *x = ReadRequestTupleKey{} + mi := &file_extention_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadRequestTupleKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadRequestTupleKey) ProtoMessage() {} + +func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. +func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{7} +} + +func (x *ReadRequestTupleKey) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *ReadRequestTupleKey) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *ReadRequestTupleKey) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +type ReadResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tuples []*Tuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"` + ContinuationToken string `protobuf:"bytes,2,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"` +} + +func (x *ReadResponse) Reset() { + *x = ReadResponse{} + mi := &file_extention_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadResponse) ProtoMessage() {} + +func (x *ReadResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. +func (*ReadResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{8} +} + +func (x *ReadResponse) GetTuples() []*Tuple { + if x != nil { + return x.Tuples + } + return nil +} + +func (x *ReadResponse) GetContinuationToken() string { + if x != nil { + return x.ContinuationToken + } + return "" +} + +type WriteRequestWrites struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TupleKeys []*TupleKey `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"` +} + +func (x *WriteRequestWrites) Reset() { + *x = WriteRequestWrites{} + mi := &file_extention_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteRequestWrites) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteRequestWrites) ProtoMessage() {} + +func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. +func (*WriteRequestWrites) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{9} +} + +func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { + if x != nil { + return x.TupleKeys + } + return nil +} + +type WriteRequestDeletes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TupleKeys []*TupleKeyWithoutCondition `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"` +} + +func (x *WriteRequestDeletes) Reset() { + *x = WriteRequestDeletes{} + mi := &file_extention_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteRequestDeletes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteRequestDeletes) ProtoMessage() {} + +func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. +func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{10} +} + +func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { + if x != nil { + return x.TupleKeys + } + return nil +} + +type WriteRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Use namespace instead of store id. It will be handled on the server side + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Writes *WriteRequestWrites `protobuf:"bytes,2,opt,name=writes,proto3" json:"writes,omitempty"` + Deletes *WriteRequestDeletes `protobuf:"bytes,3,opt,name=deletes,proto3" json:"deletes,omitempty"` +} + +func (x *WriteRequest) Reset() { + *x = WriteRequest{} + mi := &file_extention_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteRequest) ProtoMessage() {} + +func (x *WriteRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. +func (*WriteRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{11} +} + +func (x *WriteRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *WriteRequest) GetWrites() *WriteRequestWrites { + if x != nil { + return x.Writes + } + return nil +} + +func (x *WriteRequest) GetDeletes() *WriteRequestDeletes { + if x != nil { + return x.Deletes + } + return nil +} + +type WriteResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *WriteResponse) Reset() { + *x = WriteResponse{} + mi := &file_extention_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteResponse) ProtoMessage() {} + +func (x *WriteResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. +func (*WriteResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{12} +} + var File_extention_proto protoreflect.FileDescriptor var file_extention_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x22, 0x8b, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x22, 0x50, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x32, 0x62, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, - 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, - 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8b, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x14, + 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x22, 0x50, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x14, + 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, + 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, + 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, 0x52, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, 0x0b, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, + 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, + 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, + 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, + 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, + 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, 0x13, 0x57, 0x72, + 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xaf, + 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3e, 0x0a, + 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x41, 0x0a, + 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, + 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0xfb, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c, + 0x69, 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, + 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -202,19 +909,48 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_extention_proto_goTypes = []any{ - (*ListRequest)(nil), // 0: authz.extention.v1.ListRequest - (*ListResponse)(nil), // 1: authz.extention.v1.ListResponse + (*ListRequest)(nil), // 0: authz.extention.v1.ListRequest + (*ListResponse)(nil), // 1: authz.extention.v1.ListResponse + (*TupleKey)(nil), // 2: authz.extention.v1.TupleKey + (*Tuple)(nil), // 3: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 4: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 5: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 6: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 7: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 8: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 9: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 10: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 11: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 12: authz.extention.v1.WriteResponse + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 14: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 15: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ - 0, // 0: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest - 1, // 1: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 5, // 0: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 2, // 1: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 13, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 14, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 7, // 4: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 15, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 3, // 6: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 2, // 7: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 4, // 8: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 9, // 9: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 10, // 10: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 0, // 11: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest + 6, // 12: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 11, // 13: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 1, // 14: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse + 8, // 15: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 12, // 16: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 14, // [14:17] is the sub-list for method output_type + 11, // [11:14] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -228,7 +964,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_extention_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/zanzana/proto/v1/extention.proto b/pkg/services/authz/zanzana/proto/v1/extention.proto index a916f5c6f0c..0c4b67bfd23 100644 --- a/pkg/services/authz/zanzana/proto/v1/extention.proto +++ b/pkg/services/authz/zanzana/proto/v1/extention.proto @@ -4,8 +4,14 @@ option go_package = "github.com/grafana/grafana/pkg/services/authz/proto/v1"; package authz.extention.v1; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + service AuthzExtentionService { rpc List(ListRequest) returns (ListResponse); + rpc Read(ReadRequest) returns (ReadResponse); + rpc Write(WriteRequest) returns (WriteResponse); } message ListRequest { @@ -21,3 +27,62 @@ message ListResponse { repeated string folders = 2; repeated string items = 3; } + +message TupleKey { + string user = 1; + string relation = 2; + string object = 3; + RelationshipCondition condition = 4; +} + +message Tuple { + TupleKey key = 1; + google.protobuf.Timestamp timestamp = 2; +} + +message TupleKeyWithoutCondition { + string user = 1; + string relation = 2; + string object = 3; +} + +message RelationshipCondition { + string name = 1; + google.protobuf.Struct context = 2; +} + +message ReadRequest { + // Use namespace instead of store id. It will be handled on the server side + string namespace = 1; + ReadRequestTupleKey tuple_key = 2; + google.protobuf.Int32Value page_size = 3; + string continuation_token = 4; +} + +message ReadRequestTupleKey { + string user = 1; + string relation = 2; + string object = 3; +} + +message ReadResponse { + repeated Tuple tuples = 1; + string continuation_token = 2; +} + +message WriteRequestWrites { + repeated TupleKey tuple_keys = 1; +} + +message WriteRequestDeletes { + repeated TupleKeyWithoutCondition tuple_keys = 1; +} + +message WriteRequest { + // Use namespace instead of store id. It will be handled on the server side + string namespace = 1; + WriteRequestWrites writes = 2; + WriteRequestDeletes deletes = 3; +} + +message WriteResponse {} diff --git a/pkg/services/authz/zanzana/proto/v1/extention_grpc.pb.go b/pkg/services/authz/zanzana/proto/v1/extention_grpc.pb.go index e1faa1f9b86..7b73cf22306 100644 --- a/pkg/services/authz/zanzana/proto/v1/extention_grpc.pb.go +++ b/pkg/services/authz/zanzana/proto/v1/extention_grpc.pb.go @@ -19,7 +19,9 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List" + AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List" + AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read" + AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write" ) // AuthzExtentionServiceClient is the client API for AuthzExtentionService service. @@ -27,6 +29,8 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type AuthzExtentionServiceClient interface { List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) + Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) + Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) } type authzExtentionServiceClient struct { @@ -47,11 +51,33 @@ func (c *authzExtentionServiceClient) List(ctx context.Context, in *ListRequest, return out, nil } +func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReadResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Read_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authzExtentionServiceClient) Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WriteResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Write_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthzExtentionServiceServer is the server API for AuthzExtentionService service. // All implementations should embed UnimplementedAuthzExtentionServiceServer // for forward compatibility type AuthzExtentionServiceServer interface { List(context.Context, *ListRequest) (*ListResponse, error) + Read(context.Context, *ReadRequest) (*ReadResponse, error) + Write(context.Context, *WriteRequest) (*WriteResponse, error) } // UnimplementedAuthzExtentionServiceServer should be embedded to have forward compatible implementations. @@ -61,6 +87,12 @@ type UnimplementedAuthzExtentionServiceServer struct { func (UnimplementedAuthzExtentionServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method List not implemented") } +func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Read not implemented") +} +func (UnimplementedAuthzExtentionServiceServer) Write(context.Context, *WriteRequest) (*WriteResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") +} // UnsafeAuthzExtentionServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AuthzExtentionServiceServer will @@ -91,6 +123,42 @@ func _AuthzExtentionService_List_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Read(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Read_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Read(ctx, req.(*ReadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthzExtentionService_Write_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WriteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Write(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Write_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Write(ctx, req.(*WriteRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AuthzExtentionService_ServiceDesc is the grpc.ServiceDesc for AuthzExtentionService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -102,6 +170,14 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{ MethodName: "List", Handler: _AuthzExtentionService_List_Handler, }, + { + MethodName: "Read", + Handler: _AuthzExtentionService_Read_Handler, + }, + { + MethodName: "Write", + Handler: _AuthzExtentionService_Write_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "extention.proto", diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 25cb2a9508d..3505f0dcec1 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -1,35 +1 @@ package zanzana - -import ( - "fmt" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "github.com/openfga/openfga/pkg/server" - "github.com/openfga/openfga/pkg/storage" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/grpcserver" - "github.com/grafana/grafana/pkg/setting" - - zserver "github.com/grafana/grafana/pkg/services/authz/zanzana/server" -) - -func NewOpenFGAServer(cfg *setting.Cfg, store storage.OpenFGADatastore, logger log.Logger) (*server.Server, error) { - return zserver.NewOpenFGA(&cfg.Zanzana, store, logger) -} - -func NewAuthzServer(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer) (*zserver.Server, error) { - stackID := cfg.StackID - if stackID == "" { - stackID = "default" - } - - return zserver.NewAuthz( - openfga, - zserver.WithTenantID(fmt.Sprintf("stacks-%s", stackID)), - ) -} - -func StartOpenFGAHttpSever(cfg *setting.Cfg, srv grpcserver.Provider, logger log.Logger) error { - return zserver.StartOpenFGAHttpSever(cfg, srv, logger) -} diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index a9c6f0f2815..08de91b2d96 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -1,19 +1,17 @@ package server import ( - "context" "errors" - "fmt" + "sync" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/openfga/language/pkg/go/transformer" "go.opentelemetry.io/otel" - "google.golang.org/protobuf/types/known/wrapperspb" "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana/schema" + "github.com/grafana/grafana/pkg/setting" ) const ( @@ -28,6 +26,7 @@ var _ authzextv1.AuthzExtentionServiceServer = (*Server)(nil) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/server") var errStoreNotFound = errors.New("store not found") +var errAuthorizationModelNotInitialized = errors.New("authorization model not initialized") type Server struct { authzv1.UnimplementedAuthzServiceServer @@ -35,21 +34,19 @@ type Server struct { openfga openfgav1.OpenFGAServiceServer - logger log.Logger - modules []transformer.ModuleFile - tenantID string - storeID string - modelID string + logger log.Logger + modules []transformer.ModuleFile + storeMap map[string]storeInfo + storeLock *sync.Mutex +} + +type storeInfo struct { + Id string + AuthorizationModelId string } type ServerOption func(s *Server) -func WithTenantID(tenantID string) ServerOption { - return func(s *Server) { - s.tenantID = tenantID - } -} - func WithLogger(logger log.Logger) ServerOption { return func(s *Server) { s.logger = logger @@ -62,8 +59,16 @@ func WithSchema(modules []transformer.ModuleFile) ServerOption { } } +func NewAuthzServer(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer) (*Server, error) { + return NewAuthz(openfga) +} + func NewAuthz(openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Server, error) { - s := &Server{openfga: openfga} + s := &Server{ + openfga: openfga, + storeLock: &sync.Mutex{}, + storeMap: make(map[string]storeInfo), + } for _, o := range opts { o(s) @@ -73,127 +78,5 @@ func NewAuthz(openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Se s.logger = log.New("authz-server") } - if s.tenantID == "" { - s.tenantID = "stacks-default" - } - - if len(s.modules) == 0 { - s.modules = schema.SchemaModules - } - - ctx := context.Background() - store, err := s.getOrCreateStore(ctx, s.tenantID) - if err != nil { - return nil, err - } - - s.storeID = store.GetId() - - modelID, err := s.loadModel(ctx, s.storeID, s.modules) - if err != nil { - return nil, err - } - - s.modelID = modelID - return s, nil } - -func (s *Server) getOrCreateStore(ctx context.Context, name string) (*openfgav1.Store, error) { - store, err := s.getStore(ctx, name) - - if errors.Is(err, errStoreNotFound) { - var res *openfgav1.CreateStoreResponse - res, err = s.openfga.CreateStore(ctx, &openfgav1.CreateStoreRequest{Name: name}) - if res != nil { - store = &openfgav1.Store{ - Id: res.GetId(), - Name: res.GetName(), - CreatedAt: res.GetCreatedAt(), - } - } - } - - return store, err -} - -func (s *Server) getStore(ctx context.Context, name string) (*openfgav1.Store, error) { - var continuationToken string - - // OpenFGA client does not support any filters for stores. - // We should create an issue to support some way to get stores by name. - // For now we need to go thourh all stores until we find a match or we hit the end. - for { - res, err := s.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ - PageSize: &wrapperspb.Int32Value{Value: 20}, - ContinuationToken: continuationToken, - }) - - if err != nil { - return nil, fmt.Errorf("failed to initiate zanzana tenant: %w", err) - } - - for _, s := range res.GetStores() { - if s.GetName() == name { - return s, nil - } - } - - // we have no more stores to check - if res.GetContinuationToken() == "" { - return nil, errStoreNotFound - } - - continuationToken = res.GetContinuationToken() - } -} - -func (s *Server) loadModel(ctx context.Context, storeID string, modules []transformer.ModuleFile) (string, error) { - var continuationToken string - - model, err := schema.TransformModulesToModel(modules) - if err != nil { - return "", err - } - - for { - // ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation. - // So with a pageSize of 1 we will get the latest model. - res, err := s.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{ - StoreId: storeID, - PageSize: &wrapperspb.Int32Value{Value: 20}, - ContinuationToken: continuationToken, - }) - - if err != nil { - return "", fmt.Errorf("failed to load authorization model: %w", err) - } - - for _, m := range res.GetAuthorizationModels() { - // If provided dsl is equal to a stored dsl we use that as the authorization id - if schema.EqualModels(m, model) { - return m.GetId(), nil - } - } - - // If we have not found any matching authorization model we break the loop and create a new one - if res.GetContinuationToken() == "" { - break - } - - continuationToken = res.GetContinuationToken() - } - - writeRes, err := s.openfga.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{ - StoreId: s.storeID, - TypeDefinitions: model.GetTypeDefinitions(), - SchemaVersion: model.GetSchemaVersion(), - Conditions: model.GetConditions(), - }) - - if err != nil { - return "", fmt.Errorf("failed to load authorization model: %w", err) - } - - return writeRes.GetAuthorizationModelId(), nil -} diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index a5ea5823a6c..5da418c4129 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -4,9 +4,10 @@ import ( "context" authzv1 "github.com/grafana/authlib/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana/common" openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" + + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { @@ -20,12 +21,17 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C } func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) { + storeInf, err := s.getNamespaceStore(ctx, r.Namespace) + if err != nil { + return nil, err + } + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has direct access to resource res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -42,8 +48,8 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info c // 2. check if subject has access through namespace res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -58,11 +64,16 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info c } func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { + storeInf, err := s.getNamespaceStore(ctx, r.Namespace) + if err != nil { + return nil, err + } + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has direct access to resource res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -86,8 +97,8 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au // 2. check if subject has access through namespace res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -109,8 +120,8 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au // 3. check if subject has access as a sub resource for the folder res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: common.FolderResourceRelation(relation), diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index d4982c9de62..78fa4609dbb 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -14,14 +14,13 @@ import ( func testCheck(t *testing.T, server *Server) { newRead := func(subject, group, resource, folder, name string) *authzv1.CheckRequest { return &authzv1.CheckRequest{ - // FIXME: namespace should map to store - // Namespace: storeID, - Subject: subject, - Verb: utils.VerbGet, - Group: group, - Resource: resource, - Name: name, - Folder: folder, + Namespace: "default", + Subject: subject, + Verb: utils.VerbGet, + Group: group, + Resource: resource, + Name: name, + Folder: folder, } } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 09ba71c7c5b..448624abfed 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -22,13 +22,19 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext return s.listGeneric(ctx, r) } + func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info common.TypeInfo) (*authzextv1.ListResponse, error) { + storeInf, err := s.getNamespaceStore(ctx, r.Namespace) + if err != nil { + return nil, err + } + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has access through namespace because then they can read all of them res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -45,8 +51,8 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info // 2. List all resources user has access too listRes, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, Type: info.Type, Relation: relation, User: r.GetSubject(), @@ -61,12 +67,17 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info } func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) { + storeInf, err := s.getNamespaceStore(ctx, r.Namespace) + if err != nil { + return nil, err + } + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has access through namespace because then they can read all of them res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, @@ -83,8 +94,8 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a // 2. List all folders subject has access to resource type in folders, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, Type: common.TypeFolder, Relation: common.FolderResourceRelation(relation), User: r.GetSubject(), @@ -100,8 +111,8 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a // 3. List all resource directly assigned to subject direct, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ - StoreId: s.storeID, - AuthorizationModelId: s.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, Type: common.TypeResource, Relation: relation, User: r.GetSubject(), diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index a74d1d5e5fb..6df11266c1a 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -14,12 +14,11 @@ import ( func testList(t *testing.T, server *Server) { newList := func(subject, group, resource string) *authzextv1.ListRequest { return &authzextv1.ListRequest{ - // FIXME: namespace should map to store - // Namespace: storeID, - Verb: utils.VerbList, - Subject: subject, - Group: group, - Resource: resource, + Namespace: "default", + Verb: utils.VerbList, + Subject: subject, + Group: group, + Resource: resource, } } diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go new file mode 100644 index 00000000000..dc44748d9b6 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -0,0 +1,47 @@ +package server + +import ( + "context" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" +) + +func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) { + ctx, span := tracer.Start(ctx, "authzServer.Read") + defer span.End() + + storeInf, err := s.getNamespaceStore(ctx, req.Namespace) + if err != nil { + return nil, err + } + + res, err := s.openfga.Read(ctx, &openfgav1.ReadRequest{ + StoreId: storeInf.Id, + TupleKey: &openfgav1.ReadRequestTupleKey{ + User: req.GetTupleKey().GetUser(), + Relation: req.GetTupleKey().GetRelation(), + Object: req.GetTupleKey().GetObject(), + }, + PageSize: req.GetPageSize(), + ContinuationToken: req.GetContinuationToken(), + }) + if err != nil { + return nil, err + } + + tuples := make([]*authzextv1.Tuple, 0) + for _, t := range res.GetTuples() { + tuples = append(tuples, &authzextv1.Tuple{ + Key: common.ToAuthzExtTupleKey(t.GetKey()), + Timestamp: t.GetTimestamp(), + }) + } + + return &authzextv1.ReadResponse{ + Tuples: tuples, + ContinuationToken: res.GetContinuationToken(), + }, nil +} diff --git a/pkg/services/authz/zanzana/server/server_store.go b/pkg/services/authz/zanzana/server/server_store.go new file mode 100644 index 00000000000..3e760c377da --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_store.go @@ -0,0 +1,199 @@ +package server + +import ( + "context" + "errors" + "fmt" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/openfga/language/pkg/go/transformer" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/grafana/grafana/pkg/services/authz/zanzana/schema" +) + +func (s *Server) getOrCreateStore(ctx context.Context, namespace string) (*openfgav1.Store, error) { + store, err := s.getStore(ctx, namespace) + + if errors.Is(err, errStoreNotFound) { + var res *openfgav1.CreateStoreResponse + res, err = s.openfga.CreateStore(ctx, &openfgav1.CreateStoreRequest{Name: namespace}) + if res != nil { + store = &openfgav1.Store{ + Id: res.GetId(), + Name: res.GetName(), + CreatedAt: res.GetCreatedAt(), + } + s.storeMap[res.GetName()] = storeInfo{ + Id: res.GetId(), + } + } + } + + return store, err +} + +func (s *Server) getStoreInfo(namespace string) (*storeInfo, error) { + info, ok := s.storeMap[namespace] + if !ok { + return nil, errStoreNotFound + } + + return &info, nil +} + +func (s *Server) getStore(ctx context.Context, namespace string) (*openfgav1.Store, error) { + if len(s.storeMap) == 0 { + err := s.initStores(ctx) + if err != nil { + return nil, err + } + } + + storeInf, err := s.getStoreInfo(namespace) + if err != nil { + return nil, err + } + + res, err := s.openfga.GetStore(ctx, &openfgav1.GetStoreRequest{ + StoreId: storeInf.Id, + }) + if err != nil { + return nil, err + } + + store := &openfgav1.Store{ + Id: res.GetId(), + Name: res.GetName(), + CreatedAt: res.GetCreatedAt(), + } + + return store, nil +} + +func (s *Server) initStores(ctx context.Context) error { + var continuationToken string + + for { + res, err := s.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ + PageSize: &wrapperspb.Int32Value{Value: 100}, + ContinuationToken: continuationToken, + }) + + if err != nil { + return fmt.Errorf("failed to load zanzana stores: %w", err) + } + + for _, store := range res.GetStores() { + name := store.GetName() + s.storeMap[name] = storeInfo{ + Id: store.GetId(), + } + } + + // we have no more stores to check + if res.GetContinuationToken() == "" { + break + } + + continuationToken = res.GetContinuationToken() + } + + return nil +} + +func (s *Server) loadModel(ctx context.Context, namespace string, modules []transformer.ModuleFile) (string, error) { + var continuationToken string + + model, err := schema.TransformModulesToModel(modules) + if err != nil { + return "", err + } + + store, err := s.getStore(ctx, namespace) + if err != nil { + return "", err + } + + for { + // ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation. + // So with a pageSize of 1 we will get the latest model. + res, err := s.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{ + StoreId: store.GetId(), + PageSize: &wrapperspb.Int32Value{Value: 20}, + ContinuationToken: continuationToken, + }) + + if err != nil { + return "", fmt.Errorf("failed to load authorization model: %w", err) + } + + for _, m := range res.GetAuthorizationModels() { + // If provided dsl is equal to a stored dsl we use that as the authorization id + if schema.EqualModels(m, model) { + return m.GetId(), nil + } + } + + // If we have not found any matching authorization model we break the loop and create a new one + if res.GetContinuationToken() == "" { + break + } + + continuationToken = res.GetContinuationToken() + } + + writeRes, err := s.openfga.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{ + StoreId: store.GetId(), + TypeDefinitions: model.GetTypeDefinitions(), + SchemaVersion: model.GetSchemaVersion(), + Conditions: model.GetConditions(), + }) + + if err != nil { + return "", fmt.Errorf("failed to load authorization model: %w", err) + } + + return writeRes.GetAuthorizationModelId(), nil +} + +func (s *Server) getNamespaceStore(ctx context.Context, namespace string) (*storeInfo, error) { + var storeInf *storeInfo + var err error + + s.storeLock.Lock() + defer s.storeLock.Unlock() + + storeInf, err = s.getStoreInfo(namespace) + if errors.Is(err, errStoreNotFound) || storeInf.AuthorizationModelId == "" { + storeInf, err = s.initNamespaceStore(ctx, namespace) + } + if err != nil { + return nil, err + } + + return storeInf, nil +} + +func (s *Server) initNamespaceStore(ctx context.Context, namespace string) (*storeInfo, error) { + store, err := s.getOrCreateStore(ctx, namespace) + if err != nil { + return nil, err + } + + modules := schema.SchemaModules + modelID, err := s.loadModel(ctx, namespace, modules) + if err != nil { + return nil, err + } + + if info, ok := s.storeMap[store.GetName()]; ok { + s.storeMap[store.GetName()] = storeInfo{ + Id: info.Id, + AuthorizationModelId: modelID, + } + } + + updatedInfo := s.storeMap[store.GetName()] + return &updatedInfo, nil +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 0ff323bf96e..dcbe3d4cb17 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -61,10 +61,14 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { srv, err := NewAuthz(openfga) require.NoError(t, err) + namespace := "default" + storeInf, err := srv.initNamespaceStore(context.Background(), namespace) + require.NoError(t, err) + // seed tuples _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ - StoreId: srv.storeID, - AuthorizationModelId: srv.modelID, + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, Writes: &openfgav1.WriteRequestWrites{ TupleKeys: []*openfgav1.TupleKey{ common.NewResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"), diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go new file mode 100644 index 00000000000..c2b19bce4fc --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -0,0 +1,55 @@ +package server + +import ( + "context" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" +) + +func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*authzextv1.WriteResponse, error) { + ctx, span := tracer.Start(ctx, "authzServer.Write") + defer span.End() + + storeInf, err := s.getNamespaceStore(ctx, req.Namespace) + if err != nil { + return nil, err + } + if storeInf.AuthorizationModelId == "" { + return nil, errAuthorizationModelNotInitialized + } + + writeTuples := make([]*openfgav1.TupleKey, 0) + for _, t := range req.GetWrites().GetTupleKeys() { + writeTuples = append(writeTuples, common.ToOpenFGATupleKey(t)) + } + + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + for _, t := range req.GetDeletes().GetTupleKeys() { + deleteTuples = append(deleteTuples, common.ToOpenFGATupleKeyWithoutCondition(t)) + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: storeInf.Id, + AuthorizationModelId: storeInf.AuthorizationModelId, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + } + } + + _, err = s.openfga.Write(ctx, writeReq) + if err != nil { + return nil, err + } + + return &authzextv1.WriteResponse{}, nil +} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index a9b92442e57..8dbb3fcc320 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -4,8 +4,9 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/services/authz/zanzana/common" openfgav1 "github.com/openfga/api/proto/openfga/v1" + + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) const (