diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 45620c6e68f..6c1567ef469 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -6,12 +6,16 @@ import ( "fmt" "github.com/fullstorydev/grpchan/inprocgrpc" + authnlib "github.com/grafana/authlib/authn" authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/authlib/claims" "github.com/grafana/dskit/services" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -22,9 +26,12 @@ import ( 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/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" ) +const zanzanaAudience = "zanzana" + // ProvideZanzana used to register ZanzanaClient. // 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) { @@ -37,7 +44,32 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg var client zanzana.Client switch cfg.Zanzana.Mode { case setting.ZanzanaModeClient: - conn, err := grpc.NewClient(cfg.Zanzana.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ + Token: cfg.Zanzana.Token, + TokenExchangeURL: cfg.Zanzana.TokenExchangeURL, + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize token exchange client: %w", err) + } + + if cfg.StackID == "" { + return nil, fmt.Errorf("missing stack ID") + } + namespace := fmt.Sprintf("stacks-%s", cfg.StackID) + + tokenAuthCred := &tokenAuth{ + cfg: cfg, + namespace: namespace, + tokenClient: tokenClient, + } + + dialOptions := []grpc.DialOption{ + // TODO: add TLS support + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithPerRPCCredentials(tokenAuthCred), + } + + conn, err := grpc.NewClient(cfg.Zanzana.Addr, dialOptions...) if err != nil { return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } @@ -61,7 +93,18 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } + channel := &inprocgrpc.Channel{} + // Put * as a namespace so we can properly authorize request with in-proc mode + channel.WithServerUnaryInterceptor(grpcAuth.UnaryServerInterceptor(func(ctx context.Context) (context.Context, error) { + ctx = claims.WithClaims(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ + Rest: authnlib.AccessTokenClaims{ + Namespace: "*", + }, + })) + return ctx, nil + })) + openfgav1.RegisterOpenFGAServiceServer(channel, openfga) authzv1.RegisterAuthzServiceServer(channel, srv) authzextv1.RegisterAuthzExtentionServiceServer(channel, srv) @@ -134,9 +177,30 @@ func (z *Zanzana) start(ctx context.Context) error { return err } - // FIXME(kalleep): For now we use noopAuthenticator but we should create an authenticator that can be shared - // between different services. - z.handle, err = grpcserver.ProvideService(z.cfg, z.features, noopAuthenticator{}, tracer, prometheus.DefaultRegisterer) + authenticator := authnlib.NewAccessTokenAuthenticator( + authnlib.NewAccessTokenVerifier( + authnlib.VerifierConfig{ + AllowedAudiences: []string{zanzanaAudience}, + }, + authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{ + SigningKeysURL: z.cfg.Zanzana.SigningKeysURL, + }), + ), + ) + + authfn := interceptors.AuthenticatorFunc(func(ctx context.Context) (context.Context, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, fmt.Errorf("missing metadata") + } + c, err := authenticator.Authenticate(ctx, authnlib.NewGRPCTokenProvider(md)) + if err != nil { + return nil, err + } + return claims.WithClaims(ctx, c), nil + }) + + z.handle, err = grpcserver.ProvideService(z.cfg, z.features, authfn, tracer, prometheus.DefaultRegisterer) if err != nil { return fmt.Errorf("failed to create zanzana grpc server: %w", err) } @@ -175,8 +239,26 @@ func (z *Zanzana) stopping(err error) error { return nil } -type noopAuthenticator struct{} - -func (n noopAuthenticator) Authenticate(ctx context.Context) (context.Context, error) { - return ctx, nil +type tokenAuth struct { + cfg *setting.Cfg + namespace string + tokenClient *authnlib.TokenExchangeClient +} + +func (t *tokenAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { + token, err := t.tokenClient.Exchange(ctx, authnlib.TokenExchangeRequest{ + Namespace: t.namespace, + Audiences: []string{zanzanaAudience}, + }) + if err != nil { + return nil, err + } + + return map[string]string{ + authnlib.DefaultAccessTokenMetadataKey: token.Token, + }, nil +} + +func (t *tokenAuth) RequireTransportSecurity() bool { + return false } diff --git a/pkg/services/authz/zanzana/server/auth.go b/pkg/services/authz/zanzana/server/auth.go new file mode 100644 index 00000000000..c9486072994 --- /dev/null +++ b/pkg/services/authz/zanzana/server/auth.go @@ -0,0 +1,23 @@ +package server + +import ( + "context" + + "github.com/grafana/authlib/claims" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func authorize(ctx context.Context, namespace string) error { + c, ok := claims.From(ctx) + if !ok { + return status.Errorf(codes.Unauthenticated, "unauthenticated") + } + if c.GetNamespace() == "" || namespace == "" { + return status.Errorf(codes.Unauthenticated, "unauthenticated") + } + if !claims.NamespaceMatches(c.GetNamespace(), namespace) { + return status.Errorf(codes.PermissionDenied, "namespace does not match") + } + return nil +} diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index f9fd378c681..ea35b135464 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -13,6 +13,10 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest ctx, span := tracer.Start(ctx, "authzServer.BatchCheck") defer span.End() + if err := authorize(ctx, r.GetNamespace()); err != nil { + return nil, err + } + batchRes := &authzextv1.BatchCheckResponse{ Groups: make(map[string]*authzextv1.BatchCheckGroupResource), } diff --git a/pkg/services/authz/zanzana/server/server_batch_check_test.go b/pkg/services/authz/zanzana/server/server_batch_check_test.go index 23222f85157..6cb08032baa 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check_test.go +++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go @@ -1,7 +1,6 @@ package server import ( - "context" "testing" "github.com/stretchr/testify/assert" @@ -34,7 +33,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) @@ -47,7 +46,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) @@ -57,7 +56,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) @@ -70,7 +69,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "3"}, {Name: "3", Folder: "2"}, @@ -85,7 +84,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) @@ -98,7 +97,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:6 should be able to read folder 1", func(t *testing.T) { groupResource := common.FormatGroupResource(folderGroup, folderResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) @@ -111,7 +110,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) { groupResource := common.FormatGroupResource(folderGroup, folderResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) @@ -123,7 +122,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "10", Folder: "6"}, {Name: "20", Folder: "6"}, })) @@ -135,7 +134,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ {Name: "10", Folder: "6"}, {Name: "20", Folder: "6"}, })) @@ -148,7 +147,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ {Name: "10", Folder: "6"}, {Name: "11", Folder: "6"}, {Name: "12", Folder: "6"}, @@ -163,7 +162,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ {Name: "10", Folder: "6"}, {Name: "11", Folder: "6"}, {Name: "12", Folder: "6"}, @@ -178,7 +177,7 @@ func testBatchCheck(t *testing.T, server *Server) { t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) { groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ {Name: "10", Folder: "5"}, {Name: "11", Folder: "6"}, {Name: "12", Folder: "6"}, diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 90ea4fbd9a0..44bf39a8e59 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -15,6 +15,10 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C ctx, span := tracer.Start(ctx, "authzServer.Check") defer span.End() + if err := authorize(ctx, r.GetNamespace()); err != nil { + return nil, err + } + store, err := s.getStoreInfo(ctx, r.GetNamespace()) if err != nil { return nil, err diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index f1b0f550c58..38cc1198e48 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -1,7 +1,6 @@ package server import ( - "context" "testing" authzv1 "github.com/grafana/authlib/authz/proto/v1" @@ -26,130 +25,130 @@ func testCheck(t *testing.T, server *Server) { } t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) + res, err = server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) // sanity check no access to subresource - res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1")) + res, err = server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through group_resource", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) + res, err = server.Check(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2")) + res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) + res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2")) + res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "1")) + res, err := server.Check(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "10")) + res, err = server.Check(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboar in folder 6 through folder 5", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10")) + res, err := server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "11")) + res, err = server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "11")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "", "4", "12")) + res, err = server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "", "4", "12")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:9 should be able to create dashboards in folder 5", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", "")) + res, err := server.Check(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", "")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:10 should be able to read dashboard status for dashboard 10", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) + res, err := server.Check(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1")) + res, err = server.Check(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:11 should be able to read dashboard status for dashboard 10 through group_resource", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) + res, err := server.Check(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:12 should be able to read dashboard status for all dashboards in folder 5", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10")) + res, err := server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11")) + res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // inherited from folder 5 - res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12")) + res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13")) + res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 1100152de3d..c691532cbe0 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -15,6 +15,10 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis ctx, span := tracer.Start(ctx, "authzServer.List") defer span.End() + if err := authorize(ctx, r.GetNamespace()); err != nil { + return nil, err + } + store, err := s.getStoreInfo(ctx, r.Namespace) if err != nil { return nil, err diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index f1c1af4e0fb..92a115a8bed 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -1,13 +1,13 @@ package server import ( - "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -24,7 +24,7 @@ func testList(t *testing.T, server *Server) { } t.Run("user:1 should list resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:1", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) assert.Len(t, res.GetFolders(), 0) @@ -32,7 +32,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:2 should be able to list all through group", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:2", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.True(t, res.GetAll()) assert.Len(t, res.GetItems(), 0) @@ -40,7 +40,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:3 should be able to list resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:3", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) @@ -49,7 +49,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:4 should be able to list all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:4", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 2) @@ -59,7 +59,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:5 should be list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:5", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 1) @@ -67,7 +67,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:6 should be able to list folder 1", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:6", folderGroup, folderResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) assert.Len(t, res.GetFolders(), 0) @@ -75,7 +75,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:7 should be able to list all folders", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:7", folderGroup, folderResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 0) @@ -83,7 +83,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:8 should be able to list resoruce:dashboard.grafana.app/dashboard in folder 6 and folder 5", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:8", dashboardGroup, dashboardResource, "")) + res, err := server.List(newContextWithNamespace(), newList("user:8", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetFolders(), 2) @@ -92,7 +92,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:10 should be able to get resoruce:dashboard.grafana.app/dashboard/status for 10 and 11", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource)) + res, err := server.List(newContextWithNamespace(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource)) require.NoError(t, err) assert.Len(t, res.GetFolders(), 0) assert.Len(t, res.GetItems(), 2) @@ -102,7 +102,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:11 should be able to list all resoruce:dashboard.grafana.app/dashboard/status ", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource)) + res, err := server.List(newContextWithNamespace(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource)) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 0) @@ -110,7 +110,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:12 should be able to list all resoruce:dashboard.grafana.app/dashboard/status in folder 5 and 6", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource)) + res, err := server.List(newContextWithNamespace(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource)) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 2) diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index 1fc8100d2c9..c32b79c8359 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -13,6 +13,10 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze ctx, span := tracer.Start(ctx, "authzServer.Read") defer span.End() + if err := authorize(ctx, req.GetNamespace()); err != nil { + return nil, err + } + storeInf, err := s.getStoreInfo(ctx, req.Namespace) if err != nil { return nil, err diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index cf77dd52615..843c893b6b8 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + authnlib "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/claims" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/require" @@ -102,3 +104,13 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { require.NoError(t, err) return srv } + +func newContextWithNamespace() context.Context { + ctx := context.Background() + ctx = claims.WithClaims(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ + Rest: authnlib.AccessTokenClaims{ + Namespace: "*", + }, + })) + return ctx +} diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index ec99e047123..c36ae53a4cf 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -13,6 +13,10 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth ctx, span := tracer.Start(ctx, "authzServer.Write") defer span.End() + if err := authorize(ctx, req.GetNamespace()); err != nil { + return nil, err + } + storeInf, err := s.getStoreInfo(ctx, req.Namespace) if err != nil { return nil, err diff --git a/pkg/services/grpcserver/interceptors/auth.go b/pkg/services/grpcserver/interceptors/auth.go index ea81d45153f..1e6a2aa31ea 100644 --- a/pkg/services/grpcserver/interceptors/auth.go +++ b/pkg/services/grpcserver/interceptors/auth.go @@ -21,6 +21,12 @@ type Authenticator interface { Authenticate(ctx context.Context) (context.Context, error) } +type AuthenticatorFunc func(context.Context) (context.Context, error) + +func (fn AuthenticatorFunc) Authenticate(ctx context.Context) (context.Context, error) { + return fn(ctx) +} + // authenticator can authenticate GRPC requests. type authenticator struct { contextHandler grpccontext.ContextHandler diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go index e507c9706ea..e8c7ecb3756 100644 --- a/pkg/setting/settings_zanzana.go +++ b/pkg/setting/settings_zanzana.go @@ -37,6 +37,13 @@ type ZanzanaSettings struct { // Use streamed version of list objects. // Returns full list of objects, but takes more time. UseStreamedListObjects bool + + // Token used to perform the exchange request. + Token string + // URL called to perform exchange request. + TokenExchangeURL string + // URL for signing keys + SigningKeysURL string } func (cfg *Cfg) readZanzanaSettings() { @@ -63,5 +70,9 @@ func (cfg *Cfg) readZanzanaSettings() { s.ListObjectsMaxResults = uint32(sec.Key("list_objects_max_results").MustUint(1000)) s.UseStreamedListObjects = sec.Key("use_streamed_list_objects").MustBool(false) + s.Token = sec.Key("token").MustString("") + s.TokenExchangeURL = sec.Key("token_exchange_url").MustString("") + s.SigningKeysURL = sec.Key("signing_keys_url").MustString("") + cfg.Zanzana = s }