IAM: Authorize writes to zanzana on token permissions (#115645)
* validate writes to zanzana, not reads * lint ignore
This commit is contained in:
@@ -78,6 +78,9 @@ func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, fea
|
||||
ctx = types.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
Namespace: "*",
|
||||
Permissions: []string{
|
||||
zanzana.TokenPermissionUpdate,
|
||||
},
|
||||
},
|
||||
}))
|
||||
return ctx, nil
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -30,3 +32,20 @@ func authorize(ctx context.Context, namespace string, ss setting.ZanzanaServerSe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorizeWrite(ctx context.Context, namespace string, ss setting.ZanzanaServerSettings) error {
|
||||
if err := authorize(ctx, namespace, ss); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, ok := claims.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return status.Errorf(codes.Unauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
if !slices.Contains(c.GetTokenPermissions(), zanzana.TokenPermissionUpdate) {
|
||||
return status.Errorf(codes.PermissionDenied, "missing token permission %s", zanzana.TokenPermissionUpdate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) {
|
||||
b.Logf("Total tuples to write: %d", len(allTuples))
|
||||
|
||||
// Get store info
|
||||
ctx := newContextWithNamespace()
|
||||
ctx := newContextWithZanzanaUpdatePermission()
|
||||
storeInf, err := srv.getStoreInfo(ctx, benchNamespace)
|
||||
require.NoError(b, err)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
)
|
||||
@@ -35,6 +36,9 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
if _, ok := status.FromError(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform mutate request")
|
||||
}
|
||||
@@ -43,7 +47,7 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
}
|
||||
|
||||
func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
|
||||
if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil {
|
||||
if err := authorizeWrite(ctx, req.GetNamespace(), s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func testMutateFolders(t *testing.T, srv *Server) {
|
||||
setupMutateFolders(t, srv)
|
||||
|
||||
t.Run("should create new folder parent relation", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -61,7 +61,7 @@ func testMutateFolders(t *testing.T, srv *Server) {
|
||||
})
|
||||
|
||||
t.Run("should delete folder parent relation", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -88,7 +88,7 @@ func testMutateFolders(t *testing.T, srv *Server) {
|
||||
})
|
||||
|
||||
t.Run("should clean up all parent relations", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -115,7 +115,7 @@ func testMutateFolders(t *testing.T, srv *Server) {
|
||||
})
|
||||
|
||||
t.Run("should perform batch mutate if multiple operations are provided", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
|
||||
setupMutateOrgRoles(t, srv)
|
||||
|
||||
t.Run("should update user org role and delete old role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -63,7 +63,7 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
|
||||
})
|
||||
|
||||
t.Run("should add user org role and delete old role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ func testMutateResourcePermissions(t *testing.T, srv *Server) {
|
||||
setupMutateResourcePermissions(t, srv)
|
||||
|
||||
t.Run("should create new resource permission", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -76,7 +76,7 @@ func testMutateResourcePermissions(t *testing.T, srv *Server) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 2)
|
||||
|
||||
_, err = srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err = srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ func testMutateRoleBindings(t *testing.T, srv *Server) {
|
||||
setupMutateRoleBindings(t, srv)
|
||||
|
||||
t.Run("should update user role and delete old role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -75,7 +75,7 @@ func testMutateRoleBindings(t *testing.T, srv *Server) {
|
||||
})
|
||||
|
||||
t.Run("should assign role to basic role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ func testMutateRoles(t *testing.T, srv *Server) {
|
||||
setupMutateRoles(t, srv)
|
||||
|
||||
t.Run("should update role and delete old role permissions", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ func testMutateTeamBindings(t *testing.T, srv *Server) {
|
||||
setupMutateTeamBindings(t, srv)
|
||||
|
||||
t.Run("should update user team binding and delete old team binding", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
@@ -33,7 +35,7 @@ func testMutate(t *testing.T, srv *Server) {
|
||||
setupMutate(t, srv)
|
||||
|
||||
t.Run("should perform multiple mutate operations", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
_, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
@@ -133,6 +135,25 @@ func testMutate(t *testing.T, srv *Server) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 0)
|
||||
})
|
||||
|
||||
t.Run("should reject mutate without zanzana:update", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
Operation: &v1.MutateOperation_SetFolderParent{
|
||||
SetFolderParent: &v1.SetFolderParentOperation{
|
||||
Folder: "new-folder",
|
||||
Parent: "1",
|
||||
DeleteExisting: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, codes.PermissionDenied, status.Code(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeduplicateTupleKeys(t *testing.T) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -218,11 +219,21 @@ func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKe
|
||||
}
|
||||
|
||||
func newContextWithNamespace() context.Context {
|
||||
return newContextWithNamespaceAndPermissions()
|
||||
}
|
||||
|
||||
func newContextWithNamespaceAndPermissions(perms ...string) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = claims.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
Namespace: "*",
|
||||
Namespace: "*",
|
||||
Permissions: perms,
|
||||
DelegatedPermissions: perms,
|
||||
},
|
||||
}))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func newContextWithZanzanaUpdatePermission() context.Context {
|
||||
return newContextWithNamespaceAndPermissions(zanzana.TokenPermissionUpdate)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -25,6 +26,9 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
if _, ok := status.FromError(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform write request")
|
||||
}
|
||||
@@ -33,7 +37,7 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
|
||||
}
|
||||
|
||||
func (s *Server) write(ctx context.Context, req *authzextv1.WriteRequest) (*authzextv1.WriteResponse, error) {
|
||||
if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil {
|
||||
if err := authorizeWrite(ctx, req.GetNamespace(), s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteAuthorization(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
testStore := sqlstore.NewTestStore(t, sqlstore.WithCfg(cfg))
|
||||
srv := setupOpenFGAServer(t, testStore, cfg)
|
||||
setup(t, srv)
|
||||
|
||||
req := &authzextv1.WriteRequest{
|
||||
Namespace: namespace,
|
||||
Writes: &authzextv1.WriteRequestWrites{
|
||||
TupleKeys: []*authzextv1.TupleKey{
|
||||
{
|
||||
// Folder parent tuples are valid without any relationship condition.
|
||||
User: "folder:1",
|
||||
Relation: common.RelationParent,
|
||||
Object: "folder:write-authz-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("denies Write without zanzana:update", func(t *testing.T) {
|
||||
_, err := srv.Write(newContextWithNamespace(), req)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, codes.PermissionDenied, status.Code(err))
|
||||
})
|
||||
|
||||
t.Run("allows Write with zanzana:update", func(t *testing.T) {
|
||||
_, err := srv.Write(newContextWithZanzanaUpdatePermission(), req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const (
|
||||
TypeNamespace = common.TypeGroupResouce
|
||||
)
|
||||
|
||||
// TokenPermissionUpdate is required for callers to perform write operations against Zanzana (Mutate/Write).
|
||||
const TokenPermissionUpdate = "zanzana:update" //nolint:gosec // G101: permission identifier, not a credential.
|
||||
|
||||
const (
|
||||
RelationTeamMember = common.RelationTeamMember
|
||||
RelationTeamAdmin = common.RelationTeamAdmin
|
||||
|
||||
Reference in New Issue
Block a user