resource: Make resource write tenancy aware (#18423)

This commit is contained in:
Semir Patel 2023-08-10 09:53:38 -05:00 committed by GitHub
parent 10f69d86d0
commit bee12c6b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 413 additions and 116 deletions

View File

@ -6,6 +6,17 @@
package consul package consul
func (b *V1TenancyBridge) PartitionExists(partition string) (bool, error) {
if partition == "default" {
return true, nil
}
return false, nil
}
func (b *V1TenancyBridge) IsPartitionMarkedForDeletion(partition string) (bool, error) {
return false, nil
}
func (b *V1TenancyBridge) NamespaceExists(partition, namespace string) (bool, error) { func (b *V1TenancyBridge) NamespaceExists(partition, namespace string) (bool, error) {
if partition == "default" && namespace == "default" { if partition == "default" && namespace == "default" {
return true, nil return true, nil
@ -13,9 +24,6 @@ func (b *V1TenancyBridge) NamespaceExists(partition, namespace string) (bool, er
return false, nil return false, nil
} }
func (b *V1TenancyBridge) PartitionExists(partition string) (bool, error) { func (b *V1TenancyBridge) IsNamespaceMarkedForDeletion(partition, namespace string) (bool, error) {
if partition == "default" {
return true, nil
}
return false, nil return false, nil
} }

View File

@ -37,7 +37,7 @@ func (s *Server) Delete(ctx context.Context, req *pbresource.DeleteRequest) (*pb
} }
// TODO(spatel): Refactor _ and entMeta in NET-4919 // TODO(spatel): Refactor _ and entMeta in NET-4919
authz, _, err := s.getAuthorizer(tokenFromContext(ctx), acl.DefaultEnterpriseMeta()) authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), acl.DefaultEnterpriseMeta())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,7 +58,7 @@ func (s *Server) Delete(ctx context.Context, req *pbresource.DeleteRequest) (*pb
} }
// Check ACLs // Check ACLs
err = reg.ACLs.Write(authz, existing) err = reg.ACLs.Write(authz, authzContext, existing)
switch { switch {
case acl.IsErrPermissionDenied(err): case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error()) return nil, status.Error(codes.PermissionDenied, err.Error())

View File

@ -9,6 +9,54 @@ type MockTenancyBridge struct {
mock.Mock mock.Mock
} }
// IsNamespaceMarkedForDeletion provides a mock function with given fields: partition, namespace
func (_m *MockTenancyBridge) IsNamespaceMarkedForDeletion(partition string, namespace string) (bool, error) {
ret := _m.Called(partition, namespace)
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(string, string) (bool, error)); ok {
return rf(partition, namespace)
}
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(partition, namespace)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(partition, namespace)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IsPartitionMarkedForDeletion provides a mock function with given fields: partition
func (_m *MockTenancyBridge) IsPartitionMarkedForDeletion(partition string) (bool, error) {
ret := _m.Called(partition)
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(string) (bool, error)); ok {
return rf(partition)
}
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(partition)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(partition)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NamespaceExists provides a mock function with given fields: partition, namespace // NamespaceExists provides a mock function with given fields: partition, namespace
func (_m *MockTenancyBridge) NamespaceExists(partition string, namespace string) (bool, error) { func (_m *MockTenancyBridge) NamespaceExists(partition string, namespace string) (bool, error) {
ret := _m.Called(partition, namespace) ret := _m.Called(partition, namespace)

View File

@ -54,7 +54,7 @@ func (s *Server) Read(ctx context.Context, req *pbresource.ReadRequest) (*pbreso
} }
// Check V1 tenancy exists for the V2 resource. // Check V1 tenancy exists for the V2 resource.
if err = v1TenancyExists(reg, s.V1TenancyBridge, req.Id.Tenancy); err != nil { if err = v1TenancyExists(reg, s.V1TenancyBridge, req.Id.Tenancy, codes.NotFound); err != nil {
return nil, err return nil, err
} }

View File

@ -161,22 +161,22 @@ func TestRead_Success(t *testing.T) {
"namespaced resource provides nonempty partition and namespace": func(artistId, recordLabelId *pbresource.ID) *pbresource.ID { "namespaced resource provides nonempty partition and namespace": func(artistId, recordLabelId *pbresource.ID) *pbresource.ID {
return artistId return artistId
}, },
"namespaced resource provides uppercase namespace and partition": func(artistId, _ *pbresource.ID) *pbresource.ID { "namespaced resource provides uppercase partition and namespace": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId) id := clone(artistId)
id.Tenancy.Partition = strings.ToUpper(artistId.Tenancy.Partition) id.Tenancy.Partition = strings.ToUpper(artistId.Tenancy.Partition)
id.Tenancy.Namespace = strings.ToUpper(artistId.Tenancy.Namespace) id.Tenancy.Namespace = strings.ToUpper(artistId.Tenancy.Namespace)
return id return id
}, },
"namespaced resource inherits tokens namespace when empty": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Tenancy.Namespace = ""
return id
},
"namespaced resource inherits tokens partition when empty": func(artistId, _ *pbresource.ID) *pbresource.ID { "namespaced resource inherits tokens partition when empty": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId) id := clone(artistId)
id.Tenancy.Partition = "" id.Tenancy.Partition = ""
return id return id
}, },
"namespaced resource inherits tokens namespace when empty": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Tenancy.Namespace = ""
return id
},
"namespaced resource inherits tokens partition and namespace when empty": func(artistId, _ *pbresource.ID) *pbresource.ID { "namespaced resource inherits tokens partition and namespace when empty": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId) id := clone(artistId)
id.Tenancy.Partition = "" id.Tenancy.Partition = ""

View File

@ -54,7 +54,9 @@ type ACLResolver interface {
//go:generate mockery --name TenancyBridge --inpackage //go:generate mockery --name TenancyBridge --inpackage
type TenancyBridge interface { type TenancyBridge interface {
PartitionExists(partition string) (bool, error) PartitionExists(partition string) (bool, error)
NamespaceExists(partition string, namespace string) (bool, error) IsPartitionMarkedForDeletion(partition string) (bool, error)
NamespaceExists(partition, namespace string) (bool, error)
IsNamespaceMarkedForDeletion(partition, namespace string) (bool, error)
} }
func NewServer(cfg Config) *Server { func NewServer(cfg Config) *Server {
@ -145,14 +147,15 @@ func validateId(id *pbresource.ID, errorPrefix string) error {
return nil return nil
} }
func v1TenancyExists(reg *resource.Registration, v1Bridge TenancyBridge, tenancy *pbresource.Tenancy) error { // v1TenancyExists return an error with the passed in gRPC status code when tenancy partition or namespace do not exist.
func v1TenancyExists(reg *resource.Registration, v1Bridge TenancyBridge, tenancy *pbresource.Tenancy, errCode codes.Code) error {
if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace { if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
exists, err := v1Bridge.PartitionExists(tenancy.Partition) exists, err := v1Bridge.PartitionExists(tenancy.Partition)
switch { switch {
case err != nil: case err != nil:
return err return err
case !exists: case !exists:
return status.Errorf(codes.NotFound, "partition resource not found: %v", tenancy.Partition) return status.Errorf(errCode, "partition resource not found: %v", tenancy.Partition)
} }
} }
@ -162,7 +165,31 @@ func v1TenancyExists(reg *resource.Registration, v1Bridge TenancyBridge, tenancy
case err != nil: case err != nil:
return err return err
case !exists: case !exists:
return status.Errorf(codes.NotFound, "namespace resource not found: %v", tenancy.Namespace) return status.Errorf(errCode, "namespace resource not found: %v", tenancy.Namespace)
}
}
return nil
}
// v1TenancyMarkedForDeletion returns a gRPC InvalidArgument when either partition or namespace is marked for deletion.
func v1TenancyMarkedForDeletion(reg *resource.Registration, v1Bridge TenancyBridge, tenancy *pbresource.Tenancy) error {
if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
marked, err := v1Bridge.IsPartitionMarkedForDeletion(tenancy.Partition)
switch {
case err != nil:
return err
case marked:
return status.Errorf(codes.InvalidArgument, "partition marked for deletion: %v", tenancy.Partition)
}
}
if reg.Scope == resource.ScopeNamespace {
marked, err := v1Bridge.IsNamespaceMarkedForDeletion(tenancy.Partition, tenancy.Namespace)
switch {
case err != nil:
return err
case marked:
return status.Errorf(codes.InvalidArgument, "namespace marked for deletion: %v", tenancy.Namespace)
} }
} }
return nil return nil

View File

@ -80,6 +80,8 @@ func testServer(t *testing.T) *Server {
mockTenancyBridge.On("NamespaceExists", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(true, nil) mockTenancyBridge.On("NamespaceExists", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(true, nil)
mockTenancyBridge.On("PartitionExists", mock.Anything).Return(false, nil) mockTenancyBridge.On("PartitionExists", mock.Anything).Return(false, nil)
mockTenancyBridge.On("NamespaceExists", mock.Anything, mock.Anything).Return(false, nil) mockTenancyBridge.On("NamespaceExists", mock.Anything, mock.Anything).Return(false, nil)
mockTenancyBridge.On("IsPartitionMarkedForDeletion", resource.DefaultPartitionName).Return(false, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(false, nil)
return NewServer(Config{ return NewServer(Config{
Logger: testutil.Logger(t), Logger: testutil.Logger(t),

View File

@ -72,6 +72,8 @@ func RunResourceServiceWithACL(t *testing.T, aclResolver svc.ACLResolver, regist
mockTenancyBridge := &svc.MockTenancyBridge{} mockTenancyBridge := &svc.MockTenancyBridge{}
mockTenancyBridge.On("PartitionExists", resource.DefaultPartitionName).Return(true, nil) mockTenancyBridge.On("PartitionExists", resource.DefaultPartitionName).Return(true, nil)
mockTenancyBridge.On("NamespaceExists", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(true, nil) mockTenancyBridge.On("NamespaceExists", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(true, nil)
mockTenancyBridge.On("IsPartitionMarkedForDeletion", resource.DefaultPartitionName).Return(false, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", resource.DefaultPartitionName, resource.DefaultNamespaceName).Return(false, nil)
svc.NewServer(svc.Config{ svc.NewServer(svc.Config{
Backend: backend, Backend: backend,

View File

@ -37,23 +37,20 @@ import (
var errUseWriteStatus = status.Error(codes.InvalidArgument, "resource.status can only be set using the WriteStatus endpoint") var errUseWriteStatus = status.Error(codes.InvalidArgument, "resource.status can only be set using the WriteStatus endpoint")
func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbresource.WriteResponse, error) { func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbresource.WriteResponse, error) {
if err := validateWriteRequest(req); err != nil { reg, err := s.validateWriteRequest(req)
return nil, err
}
reg, err := s.resolveType(req.Resource.Id.Type)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO(spatel): Refactor _ and entMeta as part of NET-4911 v1EntMeta := v2TenancyToV1EntMeta(req.Resource.Id.Tenancy)
authz, _, err := s.getAuthorizer(tokenFromContext(ctx), acl.DefaultEnterpriseMeta()) authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), v1EntMeta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
v1EntMetaToV2Tenancy(reg, v1EntMeta, req.Resource.Id.Tenancy)
// check acls // ACL check comes before tenancy existence checks to not leak tenancy "existence".
err = reg.ACLs.Write(authz, req.Resource) err = reg.ACLs.Write(authz, authzContext, req.Resource)
switch { switch {
case acl.IsErrPermissionDenied(err): case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error()) return nil, status.Error(codes.PermissionDenied, err.Error())
@ -73,6 +70,16 @@ func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbre
) )
} }
// Check V1 tenancy exists for the V2 resource
if err = v1TenancyExists(reg, s.V1TenancyBridge, req.Resource.Id.Tenancy, codes.InvalidArgument); err != nil {
return nil, err
}
// Check V1 tenancy not marked for deletion.
if err = v1TenancyMarkedForDeletion(reg, s.V1TenancyBridge, req.Resource.Id.Tenancy); err != nil {
return nil, err
}
if err = reg.Mutate(req.Resource); err != nil { if err = reg.Mutate(req.Resource); err != nil {
return nil, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error()) return nil, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error())
} }
@ -258,7 +265,7 @@ func (s *Server) retryCAS(ctx context.Context, vsn string, cas func() error) err
return err return err
} }
func validateWriteRequest(req *pbresource.WriteRequest) error { func (s *Server) validateWriteRequest(req *pbresource.WriteRequest) (*resource.Registration, error) {
var field string var field string
switch { switch {
case req.Resource == nil: case req.Resource == nil:
@ -270,17 +277,34 @@ func validateWriteRequest(req *pbresource.WriteRequest) error {
} }
if field != "" { if field != "" {
return status.Errorf(codes.InvalidArgument, "%s is required", field) return nil, status.Errorf(codes.InvalidArgument, "%s is required", field)
} }
if err := validateId(req.Resource.Id, "resource.id"); err != nil { if err := validateId(req.Resource.Id, "resource.id"); err != nil {
return err return nil, err
} }
if req.Resource.Owner != nil { if req.Resource.Owner != nil {
if err := validateId(req.Resource.Owner, "resource.owner"); err != nil { if err := validateId(req.Resource.Owner, "resource.owner"); err != nil {
return err return nil, err
} }
} }
return nil
// Check type exists.
reg, err := s.resolveType(req.Resource.Id.Type)
if err != nil {
return nil, err
}
// Check scope
if reg.Scope == resource.ScopePartition && req.Resource.Id.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"partition scoped resource %s cannot have a namespace. got: %s",
resource.ToGVK(req.Resource.Id.Type),
req.Resource.Id.Tenancy.Namespace,
)
}
return reg, nil
} }

View File

@ -5,6 +5,7 @@ package resource
import ( import (
"context" "context"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -16,11 +17,13 @@ import (
"google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/acl/resolver" "github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo" "github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/storage" "github.com/hashicorp/consul/internal/storage"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1" pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1"
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2"
"github.com/hashicorp/consul/proto/private/prototest"
) )
func TestWrite_InputValidation(t *testing.T) { func TestWrite_InputValidation(t *testing.T) {
@ -29,49 +32,56 @@ func TestWrite_InputValidation(t *testing.T) {
demo.RegisterTypes(server.Registry) demo.RegisterTypes(server.Registry)
testCases := map[string]func(*pbresource.WriteRequest){ testCases := map[string]func(artist, recordLabel *pbresource.Resource) *pbresource.Resource{
"no resource": func(req *pbresource.WriteRequest) { req.Resource = nil }, "no resource": func(artist, recordLabel *pbresource.Resource) *pbresource.Resource { return nil },
"no id": func(req *pbresource.WriteRequest) { req.Resource.Id = nil }, "no id": func(artist, _ *pbresource.Resource) *pbresource.Resource {
"no type": func(req *pbresource.WriteRequest) { req.Resource.Id.Type = nil }, artist.Id = nil
"no tenancy": func(req *pbresource.WriteRequest) { req.Resource.Id.Tenancy = nil }, return artist
"no name": func(req *pbresource.WriteRequest) { req.Resource.Id.Name = "" }, },
"no data": func(req *pbresource.WriteRequest) { req.Resource.Data = nil }, "no type": func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Type = nil
// TODO(spatel): Refactor tenancy as part of NET-4911 return artist
// },
// // clone necessary to not pollute DefaultTenancy "no tenancy": func(artist, _ *pbresource.Resource) *pbresource.Resource {
// "tenancy partition not default": func(req *pbresource.WriteRequest) { artist.Id.Tenancy = nil
// req.Resource.Id.Tenancy = clone(req.Resource.Id.Tenancy) return artist
// req.Resource.Id.Tenancy.Partition = "" },
// }, "no name": func(artist, _ *pbresource.Resource) *pbresource.Resource {
// "tenancy namespace not default": func(req *pbresource.WriteRequest) { artist.Id.Name = ""
// req.Resource.Id.Tenancy = clone(req.Resource.Id.Tenancy) return artist
// req.Resource.Id.Tenancy.Namespace = "" },
// }, "no data": func(artist, _ *pbresource.Resource) *pbresource.Resource {
// "tenancy peername not local": func(req *pbresource.WriteRequest) { artist.Data = nil
// req.Resource.Id.Tenancy = clone(req.Resource.Id.Tenancy) return artist
// req.Resource.Id.Tenancy.PeerName = "" },
// }, "wrong data type": func(artist, _ *pbresource.Resource) *pbresource.Resource {
"wrong data type": func(req *pbresource.WriteRequest) {
var err error var err error
req.Resource.Data, err = anypb.New(&pbdemov2.Album{}) artist.Data, err = anypb.New(&pbdemov2.Album{})
require.NoError(t, err) require.NoError(t, err)
return artist
}, },
"fail validation hook": func(req *pbresource.WriteRequest) { "fail validation hook": func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist := &pbdemov2.Artist{} buffer := &pbdemov2.Artist{}
require.NoError(t, req.Resource.Data.UnmarshalTo(artist)) require.NoError(t, artist.Data.UnmarshalTo(buffer))
artist.Name = "" // name cannot be empty buffer.Name = "" // name cannot be empty
require.NoError(t, req.Resource.Data.MarshalFrom(artist)) require.NoError(t, artist.Data.MarshalFrom(buffer))
return artist
}, },
"partition scope with non-empty namespace": func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
recordLabel.Id.Tenancy.Namespace = "bogus"
return recordLabel
},
// TODO(spatel): add cluster scope tests when we have an actual cluster scoped resource (e.g. partition)
} }
for desc, modFn := range testCases { for desc, modFn := range testCases {
t.Run(desc, func(t *testing.T) { t.Run(desc, func(t *testing.T) {
res, err := demo.GenerateV2Artist() artist, err := demo.GenerateV2Artist()
require.NoError(t, err) require.NoError(t, err)
req := &pbresource.WriteRequest{Resource: res} recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
modFn(req) require.NoError(t, err)
req := &pbresource.WriteRequest{Resource: modFn(artist, recordLabel)}
_, err = client.Write(testContext(t), req) _, err = client.Write(testContext(t), req)
require.Error(t, err) require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
@ -102,31 +112,6 @@ func TestWrite_OwnerValidation(t *testing.T) {
modReqFn: func(req *pbresource.WriteRequest) { req.Resource.Owner.Name = "" }, modReqFn: func(req *pbresource.WriteRequest) { req.Resource.Owner.Name = "" },
errorContains: "resource.owner.name", errorContains: "resource.owner.name",
}, },
// TODO(spatel): Refactor tenancy as part of NET-4911
//
// // clone necessary to not pollute DefaultTenancy
// "owner tenancy partition not default": {
// modReqFn: func(req *pbresource.WriteRequest) {
// req.Resource.Owner.Tenancy = clone(req.Resource.Owner.Tenancy)
// req.Resource.Owner.Tenancy.Partition = ""
// },
// errorContains: "resource.owner.tenancy.partition",
// },
// "owner tenancy namespace not default": {
// modReqFn: func(req *pbresource.WriteRequest) {
// req.Resource.Owner.Tenancy = clone(req.Resource.Owner.Tenancy)
// req.Resource.Owner.Tenancy.Namespace = ""
// },
// errorContains: "resource.owner.tenancy.namespace",
// },
// "owner tenancy peername not local": {
// modReqFn: func(req *pbresource.WriteRequest) {
// req.Resource.Owner.Tenancy = clone(req.Resource.Owner.Tenancy)
// req.Resource.Owner.Tenancy.PeerName = ""
// },
// errorContains: "resource.owner.tenancy.peername",
// },
} }
for desc, tc := range testCases { for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) { t.Run(desc, func(t *testing.T) {
@ -227,20 +212,196 @@ func TestWrite_Mutate(t *testing.T) {
require.Equal(t, pbdemov2.Genre_GENRE_DISCO, artistData.Genre) require.Equal(t, pbdemov2.Genre_GENRE_DISCO, artistData.Genre)
} }
func TestWrite_ResourceCreation_Success(t *testing.T) { func TestWrite_Create_Success(t *testing.T) {
testCases := map[string]struct {
modFn func(artist, recordLabel *pbresource.Resource) *pbresource.Resource
expectedTenancy *pbresource.Tenancy
}{
"namespaced resource provides nonempty partition and namespace": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"namespaced resource provides uppercase partition and namespace": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Partition = strings.ToUpper(artist.Id.Tenancy.Partition)
artist.Id.Tenancy.Namespace = strings.ToUpper(artist.Id.Tenancy.Namespace)
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"namespaced resource inherits tokens partition when empty": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Partition = ""
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"namespaced resource inherits tokens namespace when empty": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Namespace = ""
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"namespaced resource inherits tokens partition and namespace when empty": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Partition = ""
artist.Id.Tenancy.Namespace = ""
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"partitioned resource provides nonempty partition": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
return recordLabel
},
expectedTenancy: resource.DefaultPartitionedTenancy(),
},
"partitioned resource provides uppercase partition": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
recordLabel.Id.Tenancy.Partition = strings.ToUpper(recordLabel.Id.Tenancy.Partition)
return recordLabel
},
expectedTenancy: resource.DefaultPartitionedTenancy(),
},
"partitioned resource inherits tokens partition when empty": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
recordLabel.Id.Tenancy.Partition = ""
return recordLabel
},
expectedTenancy: resource.DefaultPartitionedTenancy(),
},
// TODO(spatel): Add cluster scope tests when we have an actual cluster scoped resource (e.g. partition)
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
server := testServer(t) server := testServer(t)
client := testClient(t, server) client := testClient(t, server)
demo.RegisterTypes(server.Registry) demo.RegisterTypes(server.Registry)
res, err := demo.GenerateV2Artist() recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
require.NoError(t, err) require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: tc.modFn(artist, recordLabel)})
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, rsp.Resource.Version, "resource should have version") require.NotEmpty(t, rsp.Resource.Version, "resource should have version")
require.NotEmpty(t, rsp.Resource.Id.Uid, "resource id should have uid") require.NotEmpty(t, rsp.Resource.Id.Uid, "resource id should have uid")
require.NotEmpty(t, rsp.Resource.Generation, "resource should have generation") require.NotEmpty(t, rsp.Resource.Generation, "resource should have generation")
prototest.AssertDeepEqual(t, tc.expectedTenancy, rsp.Resource.Id.Tenancy)
})
}
}
func TestWrite_Create_Invalid_Tenancy(t *testing.T) {
testCases := map[string]struct {
modFn func(artist, recordLabel *pbresource.Resource) *pbresource.Resource
errCode codes.Code
errContains string
}{
"namespaced resource provides nonexistant partition": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Partition = "boguspartition"
return artist
},
errCode: codes.InvalidArgument,
errContains: "partition",
},
"namespaced resource provides nonexistant namespace": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy.Namespace = "bogusnamespace"
return artist
},
errCode: codes.InvalidArgument,
errContains: "namespace",
},
"partitioned resource provides nonexistant partition": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
recordLabel.Id.Tenancy.Partition = "boguspartition"
return recordLabel
},
errCode: codes.InvalidArgument,
errContains: "partition",
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
client := testClient(t, server)
demo.RegisterTypes(server.Registry)
recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
require.NoError(t, err)
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: tc.modFn(artist, recordLabel)})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.Contains(t, err.Error(), tc.errContains)
})
}
}
func TestWrite_Tenancy_MarkedForDeletion(t *testing.T) {
// Verify resource write fails when its partition or namespace is marked for deletion.
testCases := map[string]struct {
modFn func(artist, recordLabel *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource
errContains string
}{
"namespaced resources partition marked for deletion": {
modFn: func(artist, _ *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource {
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "part1").Return(true, nil)
return artist
},
errContains: "partition marked for deletion",
},
"namespaced resources namespace marked for deletion": {
modFn: func(artist, _ *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource {
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "part1").Return(false, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", "part1", "ns1").Return(true, nil)
return artist
},
errContains: "namespace marked for deletion",
},
"partitioned resources partition marked for deletion": {
modFn: func(_, recordLabel *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource {
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "part1").Return(true, nil)
return recordLabel
},
errContains: "partition marked for deletion",
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
client := testClient(t, server)
demo.RegisterTypes(server.Registry)
recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
require.NoError(t, err)
recordLabel.Id.Tenancy.Partition = "part1"
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
artist.Id.Tenancy.Partition = "part1"
artist.Id.Tenancy.Namespace = "ns1"
mockTenancyBridge := &MockTenancyBridge{}
mockTenancyBridge.On("PartitionExists", "part1").Return(true, nil)
mockTenancyBridge.On("NamespaceExists", "part1", "ns1").Return(true, nil)
server.V1TenancyBridge = mockTenancyBridge
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: tc.modFn(artist, recordLabel, mockTenancyBridge)})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.Contains(t, err.Error(), tc.errContains)
})
}
} }
func TestWrite_CASUpdate_Success(t *testing.T) { func TestWrite_CASUpdate_Success(t *testing.T) {

View File

@ -44,10 +44,10 @@ func RegisterProxyStateTemplate(r resource.Registry) {
return nil return nil
}, },
Write: func(authorizer acl.Authorizer, p *pbresource.Resource) error { Write: func(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, p *pbresource.Resource) error {
// Require operator:write only for "break-glass" scenarios as this resource should be mostly // Require operator:write only for "break-glass" scenarios as this resource should be mostly
// managed by a controller. // managed by a controller.
return authorizer.ToAllowAuthorizer().OperatorWriteAllowed(resource.AuthorizerContext(p.Id.Tenancy)) return authorizer.ToAllowAuthorizer().OperatorWriteAllowed(authzContext)
}, },
List: func(authorizer acl.Authorizer, tenancy *pbresource.Tenancy) error { List: func(authorizer acl.Authorizer, tenancy *pbresource.Tenancy) error {
// No-op List permission as we want to default to filtering resources // No-op List permission as we want to default to filtering resources

View File

@ -85,7 +85,7 @@ func RegisterTypes(r resource.Registry) {
return authz.ToAllowAuthorizer().KeyReadAllowed(key, authzContext) return authz.ToAllowAuthorizer().KeyReadAllowed(key, authzContext)
} }
writeACL := func(authz acl.Authorizer, res *pbresource.Resource) error { writeACL := func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, res *pbresource.Resource) error {
key := fmt.Sprintf("resource/%s/%s", resource.ToGVK(res.Id.Type), res.Id.Name) key := fmt.Sprintf("resource/%s/%s", resource.ToGVK(res.Id.Type), res.Id.Name)
return authz.ToAllowAuthorizer().KeyWriteAllowed(key, &acl.AuthorizerContext{}) return authz.ToAllowAuthorizer().KeyWriteAllowed(key, &acl.AuthorizerContext{})
} }
@ -201,9 +201,7 @@ func GenerateV1RecordLabel(name string) (*pbresource.Resource, error) {
return &pbresource.Resource{ return &pbresource.Resource{
Id: &pbresource.ID{ Id: &pbresource.ID{
Type: TypeV1RecordLabel, Type: TypeV1RecordLabel,
Tenancy: &pbresource.Tenancy{ Tenancy: resource.DefaultPartitionedTenancy(),
Partition: resource.DefaultPartitionName,
},
Name: name, Name: name,
}, },
Data: data, Data: data,
@ -236,7 +234,7 @@ func GenerateV2Artist() (*pbresource.Resource, error) {
return &pbresource.Resource{ return &pbresource.Resource{
Id: &pbresource.ID{ Id: &pbresource.ID{
Type: TypeV2Artist, Type: TypeV2Artist,
Tenancy: TenancyDefault, Tenancy: resource.DefaultNamespacedTenancy(),
Name: fmt.Sprintf("%s-%s", strings.ToLower(adjective), strings.ToLower(noun)), Name: fmt.Sprintf("%s-%s", strings.ToLower(adjective), strings.ToLower(noun)),
}, },
Data: data, Data: data,
@ -279,7 +277,7 @@ func generateV2Album(artistID *pbresource.ID, rand *rand.Rand) (*pbresource.Reso
return &pbresource.Resource{ return &pbresource.Resource{
Id: &pbresource.ID{ Id: &pbresource.ID{
Type: TypeV2Album, Type: TypeV2Album,
Tenancy: artistID.Tenancy, Tenancy: clone(artistID.Tenancy),
Name: fmt.Sprintf("%s/%s-%s", artistID.Name, strings.ToLower(adjective), strings.ToLower(noun)), Name: fmt.Sprintf("%s/%s-%s", artistID.Name, strings.ToLower(adjective), strings.ToLower(noun)),
}, },
Owner: artistID, Owner: artistID,

View File

@ -62,7 +62,7 @@ type ACLHooks struct {
// Write is used to authorize Write and Delete RPCs. // Write is used to authorize Write and Delete RPCs.
// //
// If it is omitted, `operator:write` permission is assumed. // If it is omitted, `operator:write` permission is assumed.
Write func(acl.Authorizer, *pbresource.Resource) error Write func(acl.Authorizer, *acl.AuthorizerContext, *pbresource.Resource) error
// List is used to authorize List RPCs. // List is used to authorize List RPCs.
// //
@ -124,8 +124,8 @@ func (r *TypeRegistry) Register(registration Registration) {
} }
} }
if registration.ACLs.Write == nil { if registration.ACLs.Write == nil {
registration.ACLs.Write = func(authz acl.Authorizer, id *pbresource.Resource) error { registration.ACLs.Write = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.Resource) error {
return authz.ToAllowAuthorizer().OperatorWriteAllowed(&acl.AuthorizerContext{}) return authz.ToAllowAuthorizer().OperatorWriteAllowed(authzContext)
} }
} }
if registration.ACLs.List == nil { if registration.ACLs.List == nil {

View File

@ -47,8 +47,8 @@ func TestRegister_Defaults(t *testing.T) {
require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Read(testutils.ACLNoPermissions(t), nil, artist.Id))) require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Read(testutils.ACLNoPermissions(t), nil, artist.Id)))
// verify default write hook requires operator:write // verify default write hook requires operator:write
require.NoError(t, reg.ACLs.Write(testutils.ACLOperatorWrite(t), artist)) require.NoError(t, reg.ACLs.Write(testutils.ACLOperatorWrite(t), nil, artist))
require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Write(testutils.ACLNoPermissions(t), artist))) require.True(t, acl.IsErrPermissionDenied(reg.ACLs.Write(testutils.ACLNoPermissions(t), nil, artist)))
// verify default list hook requires operator:read // verify default list hook requires operator:read
require.NoError(t, reg.ACLs.List(testutils.ACLOperatorRead(t), artist.Id.Tenancy)) require.NoError(t, reg.ACLs.List(testutils.ACLOperatorRead(t), artist.Id.Tenancy))

View File

@ -43,7 +43,7 @@ func (s Scope) String() string {
panic(fmt.Sprintf("string mapping missing for scope %v", int(s))) panic(fmt.Sprintf("string mapping missing for scope %v", int(s)))
} }
// Normalize lowercases partition and namespace. // Normalize lowercases the partition and namespace.
func Normalize(tenancy *pbresource.Tenancy) { func Normalize(tenancy *pbresource.Tenancy) {
if tenancy == nil { if tenancy == nil {
return return
@ -51,3 +51,30 @@ func Normalize(tenancy *pbresource.Tenancy) {
tenancy.Partition = strings.ToLower(tenancy.Partition) tenancy.Partition = strings.ToLower(tenancy.Partition)
tenancy.Namespace = strings.ToLower(tenancy.Namespace) tenancy.Namespace = strings.ToLower(tenancy.Namespace)
} }
// DefaultClusteredTenancy returns the default tenancy for a cluster scoped resource.
func DefaultClusteredTenancy() *pbresource.Tenancy {
return &pbresource.Tenancy{
// TODO(spatel): Remove as part of "peer is not part of tenancy" ADR
PeerName: "local",
}
}
// DefaultPartitionedTenancy returns the default tenancy for a partition scoped resource.
func DefaultPartitionedTenancy() *pbresource.Tenancy {
return &pbresource.Tenancy{
Partition: DefaultPartitionName,
// TODO(spatel): Remove as part of "peer is not part of tenancy" ADR
PeerName: "local",
}
}
// DefaultNamespedTenancy returns the default tenancy for a namespace scoped resource.
func DefaultNamespacedTenancy() *pbresource.Tenancy {
return &pbresource.Tenancy{
Partition: DefaultPartitionName,
Namespace: DefaultNamespaceName,
// TODO(spatel): Remove as part of "peer is not part of tenancy" ADR
PeerName: "local",
}
}