Files
grafana/pkg/services/authz/zanzana.go
Alexander Zobnin 0aae7e01bc Zanzana: Add remote client metrics (#116012)
* Zanzana: Add remote client metrics

* fix linter
2026-01-08 15:24:54 +01:00

324 lines
11 KiB
Go

package authz
import (
"context"
"errors"
"fmt"
"time"
"github.com/fullstorydev/grpchan/inprocgrpc"
authnlib "github.com/grafana/authlib/authn"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/grpcutils"
"github.com/grafana/authlib/types"
"github.com/grafana/dskit/middleware"
"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"
"github.com/prometheus/client_golang/prometheus/promauto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
healthv1pb "google.golang.org/grpc/health/grpc_health_v1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client"
zServer "github.com/grafana/grafana/pkg/services/authz/zanzana/server"
zStore "github.com/grafana/grafana/pkg/services/authz/zanzana/store"
"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"
)
// ProvideZanzanaClient used to register ZanzanaClient.
// It will also start an embedded ZanzanaSever if mode is set to "embedded".
func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return zClient.NewNoopClient(), nil
}
switch cfg.ZanzanaClient.Mode {
case setting.ZanzanaModeClient:
zanzanaConfig := ZanzanaClientConfig{
Addr: cfg.ZanzanaClient.Addr,
Token: cfg.ZanzanaClient.Token,
TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL,
TokenNamespace: cfg.ZanzanaClient.TokenNamespace,
ServerCertFile: cfg.ZanzanaClient.ServerCertFile,
}
return NewRemoteZanzanaClient(zanzanaConfig, reg)
case setting.ZanzanaModeEmbedded:
logger := log.New("zanzana.server")
store, err := zStore.NewEmbeddedStore(cfg, db, logger)
if err != nil {
return nil, fmt.Errorf("failed to start zanzana: %w", err)
}
openfga, err := zServer.NewOpenFGAServer(cfg.ZanzanaServer, store)
if err != nil {
return nil, fmt.Errorf("failed to start zanzana: %w", err)
}
srv, err := zServer.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg)
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 = types.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
Rest: authnlib.AccessTokenClaims{
Namespace: "*",
Permissions: []string{
zanzana.TokenPermissionUpdate,
},
},
}))
return ctx, nil
}))
openfgav1.RegisterOpenFGAServiceServer(channel, openfga)
authzv1.RegisterAuthzServiceServer(channel, srv)
authzextv1.RegisterAuthzExtentionServiceServer(channel, srv)
client, err := zClient.New(channel, reg)
if err != nil {
return nil, fmt.Errorf("failed to initialize zanzana client: %w", err)
}
return client, nil
default:
return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.ZanzanaClient.Mode)
}
}
// ProvideStandaloneZanzanaClient provides a standalone Zanzana client, without registering the Zanzana service.
// Client connects to a remote Zanzana server specified in the configuration.
func ProvideStandaloneZanzanaClient(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return zClient.NewNoopClient(), nil
}
zanzanaConfig := ZanzanaClientConfig{
Addr: cfg.ZanzanaClient.Addr,
Token: cfg.ZanzanaClient.Token,
TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL,
TokenNamespace: cfg.ZanzanaClient.TokenNamespace,
ServerCertFile: cfg.ZanzanaClient.ServerCertFile,
}
return NewRemoteZanzanaClient(zanzanaConfig, reg)
}
type ZanzanaClientConfig struct {
Addr string
Token string
TokenExchangeURL string
TokenNamespace string
ServerCertFile string
}
// NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server.
func NewRemoteZanzanaClient(cfg ZanzanaClientConfig, reg prometheus.Registerer) (zanzana.Client, error) {
tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
Token: cfg.Token,
TokenExchangeURL: cfg.TokenExchangeURL,
})
if err != nil {
return nil, fmt.Errorf("failed to create token exchange client: %w", err)
}
transportCredentials := insecure.NewCredentials()
if cfg.ServerCertFile != "" {
transportCredentials, err = credentials.NewClientTLSFromFile(cfg.ServerCertFile, "")
if err != nil {
return nil, fmt.Errorf("failed to initialize TLS certificate: %w", err)
}
}
authzRequestDuration := promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
Name: "authz_zanzana_client_request_duration_seconds",
Help: "Time spent executing requests to zanzana server.",
NativeHistogramBucketFactor: 1.1,
NativeHistogramMaxBucketNumber: 160,
NativeHistogramMinResetDuration: time.Hour,
}, []string{"operation", "status_code"})
unaryInterceptors, streamInterceptors := instrument(authzRequestDuration, middleware.ReportGRPCStatusOption)
dialOptions := []grpc.DialOption{
grpc.WithTransportCredentials(transportCredentials),
grpc.WithPerRPCCredentials(
NewGRPCTokenAuth(AuthzServiceAudience, cfg.TokenNamespace, tokenClient),
),
grpc.WithChainUnaryInterceptor(unaryInterceptors...),
grpc.WithChainStreamInterceptor(streamInterceptors...),
}
conn, err := grpc.NewClient(cfg.Addr, dialOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err)
}
client, err := zClient.New(conn, reg)
if err != nil {
return nil, fmt.Errorf("failed to initialize zanzana client: %w", err)
}
return client, nil
}
type ZanzanaService interface {
services.NamedService
}
var _ ZanzanaService = (*Zanzana)(nil)
// ProvideZanzanaService is used to register zanzana as a module so we can run it seperatly from grafana.
func ProvideZanzanaService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (*Zanzana, error) {
s := &Zanzana{
cfg: cfg,
features: features,
logger: log.New("zanzana.server"),
reg: reg,
}
s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName("zanzana")
return s, nil
}
type Zanzana struct {
*services.BasicService
cfg *setting.Cfg
logger log.Logger
handle grpcserver.Provider
features featuremgmt.FeatureToggles
reg prometheus.Registerer
}
func (z *Zanzana) start(ctx context.Context) error {
tracingCfg, err := tracing.ProvideTracingConfig(z.cfg)
if err != nil {
return err
}
tracingCfg.ServiceName = "zanzana"
tracer, err := tracing.ProvideService(tracingCfg)
if err != nil {
return err
}
store, err := zStore.NewStore(z.cfg, z.logger)
if err != nil {
return fmt.Errorf("failed to initilize zanana store: %w", err)
}
openfgaServer, err := zServer.NewOpenFGAServer(z.cfg.ZanzanaServer, store)
if err != nil {
return fmt.Errorf("failed to start zanzana: %w", err)
}
zanzanaServer, err := zServer.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg)
if err != nil {
return fmt.Errorf("failed to start zanzana: %w", err)
}
var authenticatorInterceptor interceptors.Authenticator
if z.cfg.ZanzanaServer.AllowInsecure && z.cfg.Env == setting.Dev {
z.logger.Info("Allowing insecure connections to OpenFGA HTTP server")
authenticatorInterceptor = noopAuthenticator{}
} else {
z.logger.Info("Requiring secure connections to OpenFGA HTTP server")
authenticator := authnlib.NewAccessTokenAuthenticator(
authnlib.NewAccessTokenVerifier(
authnlib.VerifierConfig{AllowedAudiences: []string{AuthzServiceAudience}},
authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{
SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL,
}),
),
)
authenticatorInterceptor = interceptors.AuthenticatorFunc(
grpcutils.NewAuthenticatorInterceptor(
authenticator,
tracer,
),
)
}
z.handle, err = grpcserver.ProvideService(
z.cfg,
z.features,
authenticatorInterceptor,
tracer,
prometheus.DefaultRegisterer,
)
if err != nil {
return fmt.Errorf("failed to create zanzana grpc server: %w", err)
}
grpcServer := z.handle.GetServer()
openfgav1.RegisterOpenFGAServiceServer(grpcServer, openfgaServer)
authzv1.RegisterAuthzServiceServer(grpcServer, zanzanaServer)
authzextv1.RegisterAuthzExtentionServiceServer(grpcServer, zanzanaServer)
// register grpc health server
healthServer := zServer.NewHealthServer(zanzanaServer)
healthv1pb.RegisterHealthServer(grpcServer, healthServer)
if _, err := grpcserver.ProvideReflectionService(z.cfg, z.handle); err != nil {
return fmt.Errorf("failed to register reflection for zanzana: %w", err)
}
return nil
}
func (z *Zanzana) running(ctx context.Context) error {
if z.cfg.Env == setting.Dev && z.cfg.ZanzanaServer.OpenFGAHttpAddr != "" {
go func() {
srv, err := zServer.NewOpenFGAHttpServer(z.cfg.ZanzanaServer, z.handle)
if err != nil {
z.logger.Error("failed to create OpenFGA HTTP server", "error", err)
} else {
z.logger.Info("Starting OpenFGA HTTP server")
if z.cfg.ZanzanaServer.AllowInsecure {
z.logger.Warn("Allowing unauthenticated connections!")
}
if err := srv.ListenAndServe(); err != nil {
z.logger.Error("failed to start OpenFGA HTTP server", "error", err)
}
}
}()
}
// Run is blocking so we can just run it here
return z.handle.Run(ctx)
}
func (z *Zanzana) stopping(err error) error {
if err != nil && !errors.Is(err, context.Canceled) {
z.logger.Error("Stopping zanzana due to unexpected error", "err", err)
}
return nil
}
// TODO this impl might be more broadly useful in authlib
type noopAuthenticator struct {
}
func (n noopAuthenticator) Authenticate(ctx context.Context) (context.Context, error) {
return ctx, nil
}