Zanzana: Remove usage from legacy access control (#98883)

* Zanzana: Remove usage from legacy access control

* remove unused

* remove zanzana client from services where it's not used

* remove unused metrics

* fix linter
This commit is contained in:
Alexander Zobnin
2025-01-14 10:26:15 +01:00
committed by GitHub
parent 7480c9eb54
commit cbb688e910
58 changed files with 139 additions and 790 deletions
@@ -3,7 +3,6 @@ package acimpl
import (
"context"
"errors"
"time"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
@@ -12,54 +11,37 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
var (
errAccessNotImplemented = errors.New("access control not implemented for resource")
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/acimpl")
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/acimpl")
var _ accesscontrol.AccessControl = new(AccessControl)
func ProvideAccessControl(features featuremgmt.FeatureToggles, zclient zanzana.Client) *AccessControl {
func ProvideAccessControl(features featuremgmt.FeatureToggles) *AccessControl {
logger := log.New("accesscontrol")
var m *acMetrics
if features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
m = initMetrics()
}
return &AccessControl{
features,
logger,
accesscontrol.NewResolvers(logger),
zclient,
m,
}
}
func ProvideAccessControlTest() *AccessControl {
return ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
return ProvideAccessControl(featuremgmt.WithFeatures())
}
type AccessControl struct {
features featuremgmt.FeatureToggles
log log.Logger
resolvers accesscontrol.Resolvers
zclient zanzana.Client
metrics *acMetrics
}
func (a *AccessControl) Evaluate(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.Evaluate")
defer span.End()
if a.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return a.evaluateCompare(ctx, user, evaluator)
}
return a.evaluate(ctx, user, evaluator)
}
@@ -104,109 +86,6 @@ func (a *AccessControl) evaluate(ctx context.Context, user identity.Requester, e
return resolvedEvaluator.Evaluate(permissions), nil
}
func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.evaluateZanzana")
defer span.End()
eval, err := evaluator.MutateScopes(ctx, a.resolvers.GetScopeAttributeMutator(user.GetOrgID()))
if err != nil {
if !errors.Is(err, accesscontrol.ErrResolverNotFound) {
return false, err
}
eval = evaluator
}
return eval.EvaluateCustom(func(action string, scopes ...string) (bool, error) {
// FIXME: handle action with no scopes
if len(scopes) == 0 {
return false, nil
}
resourceScope := scopes[0]
kind, _, identifier := accesscontrol.SplitScope(resourceScope)
// Parent folder always returned by scope resolver as a second value
var parentFolder string
if len(scopes) > 1 {
_, _, parentFolder = accesscontrol.SplitScope(scopes[1])
}
req, ok := zanzana.TranslateToCheckRequest(user.GetNamespace(), action, kind, parentFolder, identifier)
if !ok {
// unsupported translation
return false, errAccessNotImplemented
}
a.log.Debug("evaluating zanzana", "user", user.GetUID(), "namespace", req.Namespace, "verb", req.Verb, "resource", req.Resource, "name", req.Name)
res, err := a.zclient.Check(ctx, user, *req)
if err != nil {
return false, err
}
return res.Allowed, nil
})
}
type evalResult struct {
runner string
decision bool
err error
duration time.Duration
}
// evaluateCompare run RBAC and zanzana checks in parallel and then compare result
func (a *AccessControl) evaluateCompare(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.evaluateCompare")
defer span.End()
res := make(chan evalResult, 2)
go func() {
timer := prometheus.NewTimer(a.metrics.mAccessEngineEvaluationsSeconds.WithLabelValues("zanzana"))
defer timer.ObserveDuration()
start := time.Now()
hasAccess, err := a.evaluateZanzana(ctx, user, evaluator)
res <- evalResult{"zanzana", hasAccess, err, time.Since(start)}
}()
go func() {
timer := prometheus.NewTimer(a.metrics.mAccessEngineEvaluationsSeconds.WithLabelValues("grafana"))
defer timer.ObserveDuration()
start := time.Now()
hasAccess, err := a.evaluate(ctx, user, evaluator)
res <- evalResult{"grafana", hasAccess, err, time.Since(start)}
}()
first, second := <-res, <-res
close(res)
if second.runner == "grafana" {
first, second = second, first
}
if !errors.Is(second.err, errAccessNotImplemented) {
if second.err != nil {
a.log.Error("zanzana evaluation failed", "error", second.err)
} else if first.decision != second.decision {
a.metrics.mZanzanaEvaluationStatusTotal.WithLabelValues("error").Inc()
a.log.Warn(
"zanzana evaluation result does not match grafana",
"grafana_decision", first.decision,
"zanana_decision", second.decision,
"grafana_ms", first.duration,
"zanzana_ms", second.duration,
"eval", evaluator.GoString(),
)
} else {
a.metrics.mZanzanaEvaluationStatusTotal.WithLabelValues("success").Inc()
a.log.Debug("zanzana evaluation is correct", "grafana_ms", first.duration, "zanzana_ms", second.duration)
}
}
return first.decision, first.err
}
func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
}
@@ -215,8 +94,6 @@ func (a *AccessControl) WithoutResolvers() accesscontrol.AccessControl {
return &AccessControl{
features: a.features,
log: a.log,
zclient: a.zclient,
metrics: a.metrics,
resolvers: accesscontrol.NewResolvers(a.log),
}
}
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
)
@@ -66,7 +65,7 @@ func TestAccessControl_Evaluate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), zanzana.NewNoopClient())
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
if tt.scopeResolver != nil {
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.scopeResolver)
@@ -1,53 +0,0 @@
package acimpl
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
)
const (
metricsSubSystem = "authz"
metricsNamespace = "grafana"
)
type acMetrics struct {
// mAccessEngineEvaluationsSeconds is a summary for evaluating access for a specific engine (RBAC and zanzana)
mAccessEngineEvaluationsSeconds *prometheus.HistogramVec
// mZanzanaEvaluationStatusTotal is a metric for zanzana evaluation status
mZanzanaEvaluationStatusTotal *prometheus.CounterVec
}
var once sync.Once
// TODO: use prometheus.Registerer
func initMetrics() *acMetrics {
m := &acMetrics{}
once.Do(func() {
m.mAccessEngineEvaluationsSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "engine_evaluations_seconds",
Help: "Histogram for evaluation time for the specific access control engine (RBAC and zanzana).",
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Buckets: prometheus.ExponentialBuckets(0.00001, 4, 10),
},
[]string{"engine"},
)
m.mZanzanaEvaluationStatusTotal = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "zanzana_evaluation_status_total",
Help: "evaluation status (success or error) for zanzana",
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
}, []string{"status"}, map[string][]string{"status": {"success", "error"}})
prometheus.MustRegister(
m.mAccessEngineEvaluationsSeconds,
m.mZanzanaEvaluationStatusTotal,
)
})
return m
}