From e709de603ddda842dcc1d3b6b7ffb61d4e17d723 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 22 Oct 2024 14:50:52 +0200 Subject: [PATCH] Chore: Init auth model on server side (#95142) * Chore: Init auth model on server side * fix linter --- pkg/services/authz/zanzana.go | 10 +- pkg/services/authz/zanzana/server.go | 14 +- .../authz/zanzana/server/authz_server.go | 173 +++++++++++++++++- 3 files changed, 190 insertions(+), 7 deletions(-) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 762ecc1403b..4b997ae6a6d 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -56,7 +56,10 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv := zanzana.NewAuthzServer(openfga) + srv, err := zanzana.NewAuthzServer(cfg, openfga) + if err != nil { + return nil, fmt.Errorf("failed to start zanzana: %w", err) + } channel := &inprocgrpc.Channel{} openfgav1.RegisterOpenFGAServiceServer(channel, openfga) authzv1.RegisterAuthzServiceServer(channel, srv) @@ -114,7 +117,10 @@ func (z *Zanzana) start(ctx context.Context) error { return fmt.Errorf("failed to start zanzana: %w", err) } - srv := zanzana.NewAuthzServer(openfga) + srv, err := zanzana.NewAuthzServer(z.cfg, openfga) + if err != nil { + return fmt.Errorf("failed to start zanzana: %w", err) + } tracingCfg, err := tracing.ProvideTracingConfig(z.cfg) if err != nil { diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 813fbba1123..18e2c76ccab 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -1,6 +1,8 @@ package zanzana import ( + "fmt" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/openfga/openfga/pkg/server" "github.com/openfga/openfga/pkg/storage" @@ -16,8 +18,16 @@ func NewOpenFGAServer(cfg *setting.Cfg, store storage.OpenFGADatastore, logger l return zserver.NewOpenFGA(&cfg.Zanzana, store, logger) } -func NewAuthzServer(openfga openfgav1.OpenFGAServiceServer) *zserver.Server { - return zserver.NewAuthz(openfga) +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("stack-%s", stackID)), + ) } func StartOpenFGAHttpSever(cfg *setting.Cfg, srv grpcserver.Provider, logger log.Logger) error { diff --git a/pkg/services/authz/zanzana/server/authz_server.go b/pkg/services/authz/zanzana/server/authz_server.go index ee9a20e5aa2..c28e281c9eb 100644 --- a/pkg/services/authz/zanzana/server/authz_server.go +++ b/pkg/services/authz/zanzana/server/authz_server.go @@ -2,12 +2,18 @@ package server import ( "context" + "errors" + "fmt" 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" ) var _ authzv1.AuthzServiceServer = (*Server)(nil) @@ -15,18 +21,179 @@ var _ authzextv1.AuthzExtentionServiceServer = (*Server)(nil) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/server") -func NewAuthz(openfga openfgav1.OpenFGAServiceServer) *Server { - return &Server{openfga: openfga} -} +var errStoreNotFound = errors.New("store not found") type Server struct { authzv1.UnimplementedAuthzServiceServer authzextv1.UnimplementedAuthzExtentionServiceServer openfga openfgav1.OpenFGAServiceServer + + logger log.Logger + modules []transformer.ModuleFile + tenantID string + storeID string + modelID 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 + } +} + +func WithSchema(modules []transformer.ModuleFile) ServerOption { + return func(s *Server) { + s.modules = modules + } +} + +func NewAuthz(openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Server, error) { + s := &Server{openfga: openfga} + + for _, o := range opts { + o(s) + } + + if s.logger == nil { + s.logger = log.New("authz-server") + } + + if s.tenantID == "" { + s.tenantID = "stack-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) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { tracer.Start(ctx, "authzServer.Check") + return &authzv1.CheckResponse{}, 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 +}