Zanzana: Setup GRPC authentication in client/server mode (#98680)

* Zanzana: Setup GRPC authentication in client/server mode

* don't use grpcutils

* refactor

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

* Add a namespace stub for in-proc mode

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

* Read parameters from config

* authorize server requests

* add namespace to the tests context

* use stack id from config

* simplify authorize func

* properly format namespace

* return Unauthenticated if namespace is empty

* use insecure cred only in dev env

* check request namespace

* Use CallCredentials API for client auth

* provide config

* fail if stack id is missing

* improve error message

* use insecure connection by default

---------

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
Alexander Zobnin 2025-01-13 10:02:15 +01:00 committed by GitHub
parent 8f79a59e1f
commit 5922015fec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 211 additions and 59 deletions

View File

@ -6,12 +6,16 @@ import (
"fmt"
"github.com/fullstorydev/grpchan/inprocgrpc"
authnlib "github.com/grafana/authlib/authn"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/claims"
"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"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
@ -22,9 +26,12 @@ import (
zserver "github.com/grafana/grafana/pkg/services/authz/zanzana/server"
"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"
)
const zanzanaAudience = "zanzana"
// ProvideZanzana used to register ZanzanaClient.
// It will also start an embedded ZanzanaSever if mode is set to "embedded".
func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) {
@ -37,7 +44,32 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg
var client zanzana.Client
switch cfg.Zanzana.Mode {
case setting.ZanzanaModeClient:
conn, err := grpc.NewClient(cfg.Zanzana.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
Token: cfg.Zanzana.Token,
TokenExchangeURL: cfg.Zanzana.TokenExchangeURL,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize token exchange client: %w", err)
}
if cfg.StackID == "" {
return nil, fmt.Errorf("missing stack ID")
}
namespace := fmt.Sprintf("stacks-%s", cfg.StackID)
tokenAuthCred := &tokenAuth{
cfg: cfg,
namespace: namespace,
tokenClient: tokenClient,
}
dialOptions := []grpc.DialOption{
// TODO: add TLS support
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithPerRPCCredentials(tokenAuthCred),
}
conn, err := grpc.NewClient(cfg.Zanzana.Addr, dialOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err)
}
@ -61,7 +93,18 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg
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 = claims.WithClaims(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
Rest: authnlib.AccessTokenClaims{
Namespace: "*",
},
}))
return ctx, nil
}))
openfgav1.RegisterOpenFGAServiceServer(channel, openfga)
authzv1.RegisterAuthzServiceServer(channel, srv)
authzextv1.RegisterAuthzExtentionServiceServer(channel, srv)
@ -134,9 +177,30 @@ func (z *Zanzana) start(ctx context.Context) error {
return err
}
// FIXME(kalleep): For now we use noopAuthenticator but we should create an authenticator that can be shared
// between different services.
z.handle, err = grpcserver.ProvideService(z.cfg, z.features, noopAuthenticator{}, tracer, prometheus.DefaultRegisterer)
authenticator := authnlib.NewAccessTokenAuthenticator(
authnlib.NewAccessTokenVerifier(
authnlib.VerifierConfig{
AllowedAudiences: []string{zanzanaAudience},
},
authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{
SigningKeysURL: z.cfg.Zanzana.SigningKeysURL,
}),
),
)
authfn := interceptors.AuthenticatorFunc(func(ctx context.Context) (context.Context, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("missing metadata")
}
c, err := authenticator.Authenticate(ctx, authnlib.NewGRPCTokenProvider(md))
if err != nil {
return nil, err
}
return claims.WithClaims(ctx, c), nil
})
z.handle, err = grpcserver.ProvideService(z.cfg, z.features, authfn, tracer, prometheus.DefaultRegisterer)
if err != nil {
return fmt.Errorf("failed to create zanzana grpc server: %w", err)
}
@ -175,8 +239,26 @@ func (z *Zanzana) stopping(err error) error {
return nil
}
type noopAuthenticator struct{}
func (n noopAuthenticator) Authenticate(ctx context.Context) (context.Context, error) {
return ctx, nil
type tokenAuth struct {
cfg *setting.Cfg
namespace string
tokenClient *authnlib.TokenExchangeClient
}
func (t *tokenAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
token, err := t.tokenClient.Exchange(ctx, authnlib.TokenExchangeRequest{
Namespace: t.namespace,
Audiences: []string{zanzanaAudience},
})
if err != nil {
return nil, err
}
return map[string]string{
authnlib.DefaultAccessTokenMetadataKey: token.Token,
}, nil
}
func (t *tokenAuth) RequireTransportSecurity() bool {
return false
}

View File

@ -0,0 +1,23 @@
package server
import (
"context"
"github.com/grafana/authlib/claims"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func authorize(ctx context.Context, namespace string) error {
c, ok := claims.From(ctx)
if !ok {
return status.Errorf(codes.Unauthenticated, "unauthenticated")
}
if c.GetNamespace() == "" || namespace == "" {
return status.Errorf(codes.Unauthenticated, "unauthenticated")
}
if !claims.NamespaceMatches(c.GetNamespace(), namespace) {
return status.Errorf(codes.PermissionDenied, "namespace does not match")
}
return nil
}

View File

@ -13,6 +13,10 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
ctx, span := tracer.Start(ctx, "authzServer.BatchCheck")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
return nil, err
}
batchRes := &authzextv1.BatchCheckResponse{
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
}

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@ -34,7 +33,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -47,7 +46,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -57,7 +56,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -70,7 +69,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "3"},
{Name: "3", Folder: "2"},
@ -85,7 +84,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -98,7 +97,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
@ -111,7 +110,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
@ -123,7 +122,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
@ -135,7 +134,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
@ -148,7 +147,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
res, err := server.BatchCheck(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "11", Folder: "6"},
{Name: "12", Folder: "6"},
@ -163,7 +162,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
res, err := server.BatchCheck(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "11", Folder: "6"},
{Name: "12", Folder: "6"},
@ -178,7 +177,7 @@ func testBatchCheck(t *testing.T, server *Server) {
t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) {
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
res, err := server.BatchCheck(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "5"},
{Name: "11", Folder: "6"},
{Name: "12", Folder: "6"},

View File

@ -15,6 +15,10 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
ctx, span := tracer.Start(ctx, "authzServer.Check")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
return nil, err
}
store, err := s.getStoreInfo(ctx, r.GetNamespace())
if err != nil {
return nil, err

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"testing"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
@ -26,130 +25,130 @@ func testCheck(t *testing.T, server *Server) {
}
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
res, err = server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
// sanity check no access to subresource
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1"))
res, err = server.Check(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through group_resource", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
res, err = server.Check(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"))
res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"))
res, err = server.Check(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
res, err := server.Check(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "10"))
res, err = server.Check(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboar in folder 6 through folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"))
res, err := server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "11"))
res, err = server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "11"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "", "4", "12"))
res, err = server.Check(newContextWithNamespace(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "", "4", "12"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:9 should be able to create dashboards in folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", ""))
res, err := server.Check(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", ""))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:10 should be able to read dashboard status for dashboard 10", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
res, err := server.Check(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1"))
res, err = server.Check(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:11 should be able to read dashboard status for dashboard 10 through group_resource", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
res, err := server.Check(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:12 should be able to read dashboard status for all dashboards in folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"))
res, err := server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11"))
res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// inherited from folder 5
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"))
res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"))
res, err = server.Check(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})

View File

@ -15,6 +15,10 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
ctx, span := tracer.Start(ctx, "authzServer.List")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
return nil, err
}
store, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err

View File

@ -1,13 +1,13 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
@ -24,7 +24,7 @@ func testList(t *testing.T, server *Server) {
}
t.Run("user:1 should list resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:1", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
assert.Len(t, res.GetFolders(), 0)
@ -32,7 +32,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:2 should be able to list all through group", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:2", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.True(t, res.GetAll())
assert.Len(t, res.GetItems(), 0)
@ -40,7 +40,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:3 should be able to list resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:3", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
@ -49,7 +49,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:4 should be able to list all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:4", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 2)
@ -59,7 +59,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:5 should be list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:5", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 1)
@ -67,7 +67,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:6 should be able to list folder 1", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:6", folderGroup, folderResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
assert.Len(t, res.GetFolders(), 0)
@ -75,7 +75,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:7 should be able to list all folders", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:7", folderGroup, folderResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 0)
@ -83,7 +83,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:8 should be able to list resoruce:dashboard.grafana.app/dashboard in folder 6 and folder 5", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:8", dashboardGroup, dashboardResource, ""))
res, err := server.List(newContextWithNamespace(), newList("user:8", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetFolders(), 2)
@ -92,7 +92,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:10 should be able to get resoruce:dashboard.grafana.app/dashboard/status for 10 and 11", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource))
res, err := server.List(newContextWithNamespace(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetFolders(), 0)
assert.Len(t, res.GetItems(), 2)
@ -102,7 +102,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:11 should be able to list all resoruce:dashboard.grafana.app/dashboard/status ", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource))
res, err := server.List(newContextWithNamespace(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 0)
@ -110,7 +110,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:12 should be able to list all resoruce:dashboard.grafana.app/dashboard/status in folder 5 and 6", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource))
res, err := server.List(newContextWithNamespace(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 2)

View File

@ -13,6 +13,10 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze
ctx, span := tracer.Start(ctx, "authzServer.Read")
defer span.End()
if err := authorize(ctx, req.GetNamespace()); err != nil {
return nil, err
}
storeInf, err := s.getStoreInfo(ctx, req.Namespace)
if err != nil {
return nil, err

View File

@ -4,6 +4,8 @@ import (
"context"
"testing"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/authlib/claims"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/stretchr/testify/require"
@ -102,3 +104,13 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
require.NoError(t, err)
return srv
}
func newContextWithNamespace() context.Context {
ctx := context.Background()
ctx = claims.WithClaims(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
Rest: authnlib.AccessTokenClaims{
Namespace: "*",
},
}))
return ctx
}

View File

@ -13,6 +13,10 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
ctx, span := tracer.Start(ctx, "authzServer.Write")
defer span.End()
if err := authorize(ctx, req.GetNamespace()); err != nil {
return nil, err
}
storeInf, err := s.getStoreInfo(ctx, req.Namespace)
if err != nil {
return nil, err

View File

@ -21,6 +21,12 @@ type Authenticator interface {
Authenticate(ctx context.Context) (context.Context, error)
}
type AuthenticatorFunc func(context.Context) (context.Context, error)
func (fn AuthenticatorFunc) Authenticate(ctx context.Context) (context.Context, error) {
return fn(ctx)
}
// authenticator can authenticate GRPC requests.
type authenticator struct {
contextHandler grpccontext.ContextHandler

View File

@ -37,6 +37,13 @@ type ZanzanaSettings struct {
// Use streamed version of list objects.
// Returns full list of objects, but takes more time.
UseStreamedListObjects bool
// Token used to perform the exchange request.
Token string
// URL called to perform exchange request.
TokenExchangeURL string
// URL for signing keys
SigningKeysURL string
}
func (cfg *Cfg) readZanzanaSettings() {
@ -63,5 +70,9 @@ func (cfg *Cfg) readZanzanaSettings() {
s.ListObjectsMaxResults = uint32(sec.Key("list_objects_max_results").MustUint(1000))
s.UseStreamedListObjects = sec.Key("use_streamed_list_objects").MustBool(false)
s.Token = sec.Key("token").MustString("")
s.TokenExchangeURL = sec.Key("token_exchange_url").MustString("")
s.SigningKeysURL = sec.Key("signing_keys_url").MustString("")
cfg.Zanzana = s
}