diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index da6aad7fc2b..28c3c5ede6a 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -2,12 +2,16 @@ package resource import ( "context" + "fmt" "log/slog" + "sync" "time" "github.com/grafana/authlib/authz" "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn/grpcutils" + "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" @@ -33,104 +37,153 @@ var _ authz.AccessClient = &staticAuthzClient{} type groupResource map[string]map[string]interface{} +const ( + metricsNamespace = "grafana" + metricsSubSystem = "grpc_authz_limited_client" +) + +var metOnce sync.Once + +type accessMetrics struct { + checkDuration *prometheus.HistogramVec + compileDuration *prometheus.HistogramVec + errorsTotal *prometheus.CounterVec +} + +func newMetrics(reg prometheus.Registerer) *accessMetrics { + m := &accessMetrics{ + checkDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "check_duration_seconds", + Help: "duration of the access check calls going through the authz service", + }, []string{"group", "resource", "verb", "allowed"}), + compileDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "compile_duration_seconds", + Help: "duration of the access compile calls going through the authz service", + }, []string{"group", "resource", "verb"}), + errorsTotal: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "errors_total", + Help: "Number of errors", + }, []string{"group", "resource", "verb"}), + } + + if reg != nil { + metOnce.Do(func() { + reg.MustRegister(m.checkDuration) + reg.MustRegister(m.compileDuration) + reg.MustRegister(m.errorsTotal) + }) + } + + return m +} + // authzLimitedClient is a client that enforces RBAC for the limited number of groups and resources. // This is a temporary solution until the authz service is fully implemented. // The authz service will be responsible for enforcing RBAC. // For now, it makes one call to the authz service for each list items. This is known to be inefficient. type authzLimitedClient struct { - client authz.AccessChecker + client authz.AccessClient // allowlist is a map of group to resources that are compatible with RBAC. allowlist groupResource logger *slog.Logger tracer trace.Tracer + metrics *accessMetrics } type AuthzOptions struct { - Tracer trace.Tracer + Tracer trace.Tracer + Registry prometheus.Registerer } // NewAuthzLimitedClient creates a new authzLimitedClient. -func NewAuthzLimitedClient(client authz.AccessChecker, opts AuthzOptions) authz.AccessClient { +func NewAuthzLimitedClient(client authz.AccessClient, opts AuthzOptions) authz.AccessClient { logger := slog.Default().With("logger", "limited-authz-client") if opts.Tracer == nil { opts.Tracer = noop.NewTracerProvider().Tracer("limited-authz-client") } + if opts.Registry == nil { + opts.Registry = prometheus.DefaultRegisterer + } return &authzLimitedClient{ client: client, allowlist: groupResource{ "dashboard.grafana.app": map[string]interface{}{"dashboards": nil}, "folder.grafana.app": map[string]interface{}{"folders": nil}, }, - logger: logger, - tracer: opts.Tracer, + logger: logger, + tracer: opts.Tracer, + metrics: newMetrics(opts.Registry), } } // Check implements authz.AccessClient. func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) { + t := time.Now() ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Check", trace.WithAttributes( attribute.String("group", req.Group), attribute.String("resource", req.Resource), - attribute.Bool("fallback", grpcutils.FallbackUsed(ctx)), + attribute.String("namespace", req.Namespace), + attribute.String("name", req.Name), + attribute.String("verb", req.Verb), + attribute.String("folder", req.Folder), + attribute.Bool("fallback_used", grpcutils.FallbackUsed(ctx)), )) defer span.End() if grpcutils.FallbackUsed(ctx) { - c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "fallback", true, "rbac", false, "allowed", true) + span.SetAttributes(attribute.Bool("allowed", true)) return authz.CheckResponse{Allowed: true}, nil } if !c.IsCompatibleWithRBAC(req.Group, req.Resource) { - c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "fallback", false, "rbac", false, "allowed", true) + span.SetAttributes(attribute.Bool("allowed", true)) return authz.CheckResponse{Allowed: true}, nil } - t := time.Now() resp, err := c.client.Check(ctx, id, req) if err != nil { - c.logger.Error("Check", "group", req.Group, "resource", req.Resource, "fallback", false, "rbac", true, "error", err, "duration", time.Since(t)) + c.logger.Error("Check", "group", req.Group, "resource", req.Resource, "error", err, "duration", time.Since(t), "traceid", tracing.TraceIDFromContext(ctx, false)) + c.metrics.errorsTotal.WithLabelValues(req.Group, req.Resource, req.Verb).Inc() + span.SetAttributes(attribute.String("error", err.Error())) return resp, err } - c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "fallback", false, "rbac", true, "allowed", resp.Allowed, "duration", time.Since(t)) + span.SetAttributes(attribute.Bool("allowed", resp.Allowed)) + c.metrics.checkDuration.WithLabelValues(req.Group, req.Resource, req.Verb, fmt.Sprintf("%t", resp.Allowed)).Observe(time.Since(t).Seconds()) return resp, nil } // Compile implements authz.AccessClient. func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) { + t := time.Now() + fallbackUsed := grpcutils.FallbackUsed(ctx) ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Compile", trace.WithAttributes( attribute.String("group", req.Group), attribute.String("resource", req.Resource), + attribute.String("namespace", req.Namespace), + attribute.String("verb", req.Verb), + attribute.Bool("fallback_used", fallbackUsed), )) defer span.End() - return func(namespace string, name, folder string) bool { - ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Compile.Check", trace.WithAttributes( - attribute.String("group", req.Group), - attribute.String("resource", req.Resource), - attribute.Bool("fallback", grpcutils.FallbackUsed(ctx)), - )) - defer span.End() - if grpcutils.FallbackUsed(ctx) { - c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "fallback", true, "rbac", false, "allowed", true) + if fallbackUsed || !c.IsCompatibleWithRBAC(req.Group, req.Resource) { + return func(namespace string, name, folder string) bool { return true - } - // TODO: Implement For now we perform the check for each item. - if !c.IsCompatibleWithRBAC(req.Group, req.Resource) { - c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "fallback", false, "rbac", false, "allowed", true) - return true - } - t := time.Now() - r, err := c.client.Check(ctx, id, authz.CheckRequest{ - Verb: "get", - Group: req.Group, - Resource: req.Resource, - Namespace: namespace, - Name: name, - Folder: folder, - }) - if err != nil { - c.logger.Error("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "fallback", false, "rbac", true, "error", err, "duration", time.Since(t)) - return false - } - c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "fallback", false, "rbac", true, "allowed", r.Allowed, "duration", time.Since(t)) - return r.Allowed - }, nil + }, nil + } + checker, err := c.client.Compile(ctx, id, req) + if err != nil { + c.logger.Error("Compile", "group", req.Group, "resource", req.Resource, "error", err, "traceid", tracing.TraceIDFromContext(ctx, false)) + c.metrics.errorsTotal.WithLabelValues(req.Group, req.Resource, req.Verb).Inc() + span.SetAttributes(attribute.String("error", err.Error())) + return nil, err + } + c.metrics.compileDuration.WithLabelValues(req.Group, req.Resource, req.Verb).Observe(time.Since(t).Seconds()) + return checker, nil } func (c authzLimitedClient) IsCompatibleWithRBAC(group, resource string) bool { diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index eca39d0e802..01b9b75322c 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -714,6 +714,7 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err Group: key.Group, Resource: key.Resource, Namespace: key.Namespace, + Verb: utils.VerbGet, }) if err != nil { return &ListResponse{Error: AsErrorResult(err)}, nil @@ -789,6 +790,7 @@ func (s *server) Restore(ctx context.Context, req *RestoreRequest) (*RestoreResp Group: req.Key.Group, Resource: req.Key.Resource, Namespace: req.Key.Namespace, + Verb: utils.VerbGet, }) if err != nil { return &RestoreResponse{Error: AsErrorResult(err)}, nil diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 0269e395961..a765d8292f6 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -33,7 +33,7 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, Reg: reg, } if ac != nil { - opts.AccessClient = resource.NewAuthzLimitedClient(ac, resource.AuthzOptions{Tracer: tracer}) + opts.AccessClient = resource.NewAuthzLimitedClient(ac, resource.AuthzOptions{Tracer: tracer, Registry: reg}) } // Support local file blob if strings.HasPrefix(opts.Blob.URL, "./data/") {