[unistore] wire authlib compile (#99027)
* Wire authz client compile method * add verb to metric * remove tipo
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/") {
|
||||
|
||||
Reference in New Issue
Block a user