Authlib: Update authz client to use zookies (#111291)

* Authlib: Update authz client to use zookies

* fix zookie return

* fix linter
This commit is contained in:
Alexander Zobnin
2025-09-18 16:24:22 +02:00
committed by GitHub
parent 0aa5dee86a
commit 72d212c5f9
28 changed files with 89 additions and 81 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.24.6
require (
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.34.1
+4 -4
View File
@@ -30,10 +30,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea h1:F9xbdvqLgeVyW+yZviJ06mH9dnFiKmkWF3wJ7AGyzc4=
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933 h1:GjiMR5NIO1/bYSCnt8x7VUeOMaupv2qXJkeLDVAddxQ=
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+2 -2
View File
@@ -4,7 +4,7 @@ go 1.24.6
require (
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781
github.com/grafana/grafana-app-sdk/logging v0.45.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.2
@@ -44,7 +44,7 @@ require (
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea // indirect
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
+4 -4
View File
@@ -63,10 +63,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea h1:F9xbdvqLgeVyW+yZviJ06mH9dnFiKmkWF3wJ7AGyzc4=
github.com/grafana/authlib v0.0.0-20250910124502-5d080d6bb9ea/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933 h1:GjiMR5NIO1/bYSCnt8x7VUeOMaupv2qXJkeLDVAddxQ=
github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w=
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"strconv"
authlib "github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
@@ -74,7 +75,7 @@ func List[T Resource](
check := func(_, _ string) bool { return true }
if ac != nil {
var err error
check, err = ac.Compile(ctx, ident, authlib.ListRequest{
check, _, err = ac.Compile(ctx, ident, authlib.ListRequest{
Resource: resource.GroupResource().Resource,
Group: resource.GroupResource().Group,
Verb: "list",
@@ -11,14 +11,15 @@ import (
"go.opentelemetry.io/otel/trace"
"k8s.io/apiserver/pkg/admission"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/codes"
"github.com/grafana/grafana-app-sdk/logging"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/service/metrics"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/codes"
)
var _ contracts.SecureValueService = (*SecureValueService)(nil)
@@ -285,7 +286,7 @@ func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace
return nil, fmt.Errorf("missing auth info in context")
}
hasPermissionFor, err := s.accessClient.Compile(ctx, user, claims.ListRequest{
hasPermissionFor, _, err := s.accessClient.Compile(ctx, user, claims.ListRequest{
Group: secretv1beta1.APIGroup,
Resource: secretv1beta1.SecureValuesResourceInfo.GetName(),
Namespace: namespace.String(),
+6 -5
View File
@@ -6,6 +6,7 @@ import (
"fmt"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
@@ -142,24 +143,24 @@ func (c *LegacyAccessClient) Check(ctx context.Context, id claims.AuthInfo, req
return claims.CheckResponse{Allowed: allowed}, nil
}
func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, error) {
func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, claims.Zookie, error) {
ident, ok := id.(identity.Requester)
if !ok {
return nil, errors.New("expected identity.Requester for legacy access control")
return nil, claims.NoopZookie{}, errors.New("expected identity.Requester for legacy access control")
}
opts, ok := c.opts[req.Resource]
if !ok {
return nil, fmt.Errorf("unsupported resource: %s", req.Resource)
return nil, claims.NoopZookie{}, fmt.Errorf("unsupported resource: %s", req.Resource)
}
action, ok := opts.Mapping[utils.VerbList]
if !ok {
return nil, fmt.Errorf("missing action for %s %s", utils.VerbList, req.Resource)
return nil, claims.NoopZookie{}, fmt.Errorf("missing action for %s %s", utils.VerbList, req.Resource)
}
check := Checker(ident, action)
return func(name, _ string) bool {
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
}, nil
}, claims.NoopZookie{}, nil
}
+1 -1
View File
@@ -43,7 +43,7 @@ func (c *Client) Check(ctx context.Context, id authlib.AuthInfo, req authlib.Che
return c.authzlibclient.Check(ctx, id, req)
}
func (c *Client) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, error) {
func (c *Client) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Compile")
defer span.End()
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
authlib "github.com/grafana/authlib/types"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)
@@ -19,8 +20,8 @@ func (nc *NoopClient) Check(ctx context.Context, id authlib.AuthInfo, req authli
return authlib.CheckResponse{}, nil
}
func (nc *NoopClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, error) {
return nil, nil
func (nc *NoopClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
return nil, authlib.NoopZookie{}, nil
}
func (nc NoopClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) {
@@ -9,6 +9,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
)
var _ authlib.AccessClient = (*ShadowClient)(nil)
type ShadowClient struct {
logger log.Logger
accessClient authlib.AccessClient
@@ -67,7 +69,7 @@ func (c *ShadowClient) Check(ctx context.Context, id authlib.AuthInfo, req authl
return res, err
}
func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, error) {
func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
zanzanaItemCheckerChan := make(chan authlib.ItemChecker, 1)
go func() {
if c.zanzanaClient == nil {
@@ -76,7 +78,7 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
}
timer := prometheus.NewTimer(c.metrics.compileSeconds.WithLabelValues("zanzana"))
itemChecker, err := c.zanzanaClient.Compile(ctx, id, req)
itemChecker, _, err := c.zanzanaClient.Compile(ctx, id, req)
timer.ObserveDuration()
if err != nil {
c.logger.Warn("Failed to compile zanzana item checker", "error", err)
@@ -85,10 +87,10 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
}()
timer := prometheus.NewTimer(c.metrics.compileSeconds.WithLabelValues("rbac"))
rbacItemChecker, err := c.accessClient.Compile(ctx, id, req)
rbacItemChecker, _, err := c.accessClient.Compile(ctx, id, req)
timer.ObserveDuration()
if err != nil {
return nil, err
return nil, authlib.NoopZookie{}, err
}
zanzanaItemChecker := <-zanzanaItemCheckerChan
@@ -107,5 +109,5 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
return rbacRes
}
return shadowItemChecker, err
return shadowItemChecker, authlib.NoopZookie{}, err
}
+8 -8
View File
@@ -159,7 +159,7 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c
}
// Compile implements claims.AccessClient.
func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, error) {
func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, claims.Zookie, error) {
t := time.Now()
fallbackUsed := FallbackUsed(ctx)
ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Compile", trace.WithAttributes(
@@ -177,34 +177,34 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
span.SetStatus(codes.Error, "Namespace empty")
err := fmt.Errorf("namespace empty")
span.RecordError(err)
return nil, err
return nil, claims.NoopZookie{}, err
}
return func(name, folder string) bool {
return true
}, nil
}, claims.NoopZookie{}, nil
}
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
span.SetAttributes(attribute.Bool("allowed", false))
span.SetStatus(codes.Error, "Namespace mismatch")
span.RecordError(claims.ErrNamespaceMismatch)
return nil, claims.ErrNamespaceMismatch
return nil, claims.NoopZookie{}, claims.ErrNamespaceMismatch
}
if !c.IsCompatibleWithRBAC(req.Group, req.Resource) {
return func(name, folder string) bool {
return true
}, nil
}, claims.NoopZookie{}, nil
}
checker, err := c.client.Compile(ctx, id, req)
checker, zookie, err := c.client.Compile(ctx, id, req)
if err != nil {
c.logger.Error("Compile", "group", req.Group, "resource", req.Resource, "error", err, "traceid", trace.SpanContextFromContext(ctx).TraceID().String())
c.metrics.errorsTotal.WithLabelValues(req.Group, req.Resource, req.Verb).Inc()
span.SetStatus(codes.Error, fmt.Sprintf("compile failed: %v", err))
span.RecordError(err)
return nil, err
return nil, zookie, err
}
c.metrics.compileDuration.WithLabelValues(req.Group, req.Resource, req.Verb).Observe(time.Since(t).Seconds())
return checker, nil
return checker, zookie, nil
}
func (c authzLimitedClient) IsCompatibleWithRBAC(group, resource string) bool {
+4 -3
View File
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
@@ -60,7 +61,7 @@ func TestAuthzLimitedClient_Compile(t *testing.T) {
Verb: utils.VerbGet,
Namespace: "stacks-1",
}
checker, err := client.Compile(context.Background(), &identity.StaticRequester{Namespace: "stacks-1"}, req)
checker, _, err := client.Compile(context.Background(), &identity.StaticRequester{Namespace: "stacks-1"}, req)
assert.NoError(t, err)
assert.NotNil(t, checker)
@@ -143,7 +144,7 @@ func TestNamespaceMatching(t *testing.T) {
Verb: utils.VerbGet,
Namespace: tt.reqNamespace,
}
_, compileErr := client.Compile(ctx, user, compileReq)
_, _, compileErr := client.Compile(ctx, user, compileReq)
if tt.expectError {
require.Error(t, checkErr, "Check should return error")
@@ -207,7 +208,7 @@ func TestNamespaceMatchingFallback(t *testing.T) {
Verb: utils.VerbGet,
Namespace: tt.reqNamespace,
}
_, compileErr := client.Compile(ctx, user, compileReq)
_, _, compileErr := client.Compile(ctx, user, compileReq)
if tt.expectError {
require.Error(t, checkErr, "Check should return error")
+1 -1
View File
@@ -189,7 +189,7 @@ func (s *server) BulkProcess(stream resourcepb.BulkStore_BulkProcessServer) erro
}
// This will be called for each request -- with the folder ID
runner.checker[NSGR(k)], err = s.access.Compile(ctx, user, authlib.ListRequest{
runner.checker[NSGR(k)], _, err = s.access.Compile(ctx, user, authlib.ListRequest{
Namespace: k.Namespace,
Group: k.Group,
Resource: k.Resource,
+3 -3
View File
@@ -952,7 +952,7 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour
rsp := &resourcepb.ListResponse{}
key := req.Options.Key
checker, err := s.access.Compile(ctx, user, claims.ListRequest{
checker, _, err := s.access.Compile(ctx, user, claims.ListRequest{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
@@ -960,7 +960,7 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour
})
var trashChecker claims.ItemChecker // only for trash
if req.Source == resourcepb.ListRequest_TRASH {
trashChecker, err = s.access.Compile(ctx, user, claims.ListRequest{
trashChecker, _, err = s.access.Compile(ctx, user, claims.ListRequest{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
@@ -1097,7 +1097,7 @@ func (s *server) Watch(req *resourcepb.WatchRequest, srv resourcepb.ResourceStor
}
key := req.Options.Key
checker, err := s.access.Compile(ctx, user, claims.ListRequest{
checker, _, err := s.access.Compile(ctx, user, claims.ListRequest{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
+2 -2
View File
@@ -1181,7 +1181,7 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R
verb = utils.VerbPatch
}
checker, err := access.Compile(ctx, auth, authlib.ListRequest{
checker, _, err := access.Compile(ctx, auth, authlib.ListRequest{
Namespace: b.key.Namespace,
Group: b.key.Group,
Resource: b.key.Resource,
@@ -1196,7 +1196,7 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R
// handle federation
for _, federated := range req.Federated {
checker, err := access.Compile(ctx, auth, authlib.ListRequest{
checker, _, err := access.Compile(ctx, auth, authlib.ListRequest{
Namespace: federated.Namespace,
Group: federated.Group,
Resource: federated.Resource,
+2 -2
View File
@@ -638,10 +638,10 @@ func (nc *StubAccessClient) Check(ctx context.Context, id authlib.AuthInfo, req
return authlib.CheckResponse{Allowed: nc.resourceResponses[req.Resource]}, nil
}
func (nc *StubAccessClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, error) {
func (nc *StubAccessClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
return func(name, folder string) bool {
return nc.resourceResponses[req.Resource]
}, nil
}, authlib.NoopZookie{}, nil
}
func (nc StubAccessClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) {
+3 -3
View File
@@ -514,9 +514,9 @@ func (m *mockAccessClient) Check(ctx context.Context, user types.AuthInfo, req t
return types.CheckResponse{Allowed: m.allowed}, nil
}
func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req types.ListRequest) (types.ItemChecker, error) {
func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
if m.compileFn != nil {
return m.compileFn(user, req), nil
return m.compileFn(user, req), types.NoopZookie{}, nil
}
return func(name, folder string) bool {
key := fmt.Sprintf("%s:%s", folder, req.Verb)
@@ -524,5 +524,5 @@ func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req
return allowed
}
return m.allowed
}, nil
}, types.NoopZookie{}, nil
}