Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion agent/grpc-external/services/resource/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ func (s *Server) validateReadRequest(req *pbresource.ReadRequest) (*resource.Reg
req.Id.Tenancy.Namespace,
)
}

if reg.Scope == resource.ScopeCluster {
if req.Id.Tenancy.Partition != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"cluster scoped resource %s cannot have a partition: %s",
resource.ToGVK(req.Id.Type),
req.Id.Tenancy.Partition,
)
}
if req.Id.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"cluster scoped resource %s cannot have a namespace: %s",
resource.ToGVK(req.Id.Type),
req.Id.Tenancy.Namespace,
)
}
}
return reg, nil
}
25 changes: 19 additions & 6 deletions agent/grpc-external/services/resource/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/storage"
"github.com/hashicorp/consul/internal/tenancy"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/hashicorp/consul/sdk/testutil"
Expand All @@ -30,22 +31,31 @@ import (
func TestRead_InputValidation(t *testing.T) {
server := testServer(t)
client := testClient(t, server)
tenancy.RegisterTypes(server.Registry)
demo.RegisterTypes(server.Registry)

testCases := map[string]func(artistId, recordlabelId *pbresource.ID) *pbresource.ID{
"no id": func(artistId, recordLabelId *pbresource.ID) *pbresource.ID { return nil },
"no type": func(artistId, _ *pbresource.ID) *pbresource.ID {
testCases := map[string]func(artistId, recordlabelId, executiveId *pbresource.ID) *pbresource.ID{
"no id": func(_, _, _ *pbresource.ID) *pbresource.ID { return nil },
"no type": func(artistId, _, _ *pbresource.ID) *pbresource.ID {
artistId.Type = nil
return artistId
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
"no name": func(artistId, _, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
},
"partition scope with non-empty namespace": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
"partition scope with non-empty namespace": func(_, recordLabelId, _ *pbresource.ID) *pbresource.ID {
recordLabelId.Tenancy.Namespace = "ishouldnothaveanamespace"
return recordLabelId
},
"cluster scope with non-empty partition": func(_, _, executiveId *pbresource.ID) *pbresource.ID {
executiveId.Tenancy = &pbresource.Tenancy{Partition: resource.DefaultPartitionName}
return executiveId
},
"cluster scope with non-empty namespace": func(_, _, executiveId *pbresource.ID) *pbresource.ID {
executiveId.Tenancy = &pbresource.Tenancy{Namespace: resource.DefaultNamespaceName}
return executiveId
},
}
for desc, modFn := range testCases {
t.Run(desc, func(t *testing.T) {
Expand All @@ -55,8 +65,11 @@ func TestRead_InputValidation(t *testing.T) {
recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
require.NoError(t, err)

executive, err := demo.GenerateV1Executive("MusicMan", "CEO")
require.NoError(t, err)

// Each test case picks which resource to use based on the resource type's scope.
req := &pbresource.ReadRequest{Id: modFn(artist.Id, recordLabel.Id)}
req := &pbresource.ReadRequest{Id: modFn(artist.Id, recordLabel.Id, executive.Id)}

_, err = client.Read(testContext(t), req)
require.Error(t, err)
Expand Down
93 changes: 56 additions & 37 deletions agent/grpc-external/services/resource/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,59 +54,78 @@ func AuthorizerFrom(t *testing.T, policyStrs ...string) resolver.Result {
// returns a client to interact with it. ACLs will be disabled and only the
// default partition and namespace are available.
func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient {
// Provide a resolver which will default partition and namespace when not provided. This is similar to user
// initiated requests.
//
// Controllers under test should be providing full tenancy since they will run with the DANGER_NO_AUTH.
mockACLResolver := &svc.MockACLResolver{}
mockACLResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything).
Return(testutils.ACLsDisabled(t), nil).
Run(func(args mock.Arguments) {
// Caller expecting passed in tokenEntMeta and authorizerContext to be filled in.
tokenEntMeta := args.Get(1).(*acl.EnterpriseMeta)
if tokenEntMeta != nil {
FillEntMeta(tokenEntMeta)
}

authzContext := args.Get(2).(*acl.AuthorizerContext)
if authzContext != nil {
FillAuthorizerContext(authzContext)
}
})

return RunResourceServiceWithACL(t, mockACLResolver, registerFns...)
return RunResourceServiceWithConfig(t, svc.Config{}, registerFns...)
}

func RunResourceServiceWithACL(t *testing.T, aclResolver svc.ACLResolver, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient {
// RunResourceServiceWithConfig runs a ResourceService with caller injectable config to ease mocking dependencies.
// Any nil config field is replaced with a reasonable default with the following behavior:
//
// config.Backend - cannot be configured and must be nil
// config.Registry - empty registry
// config.TenancyBridge - mock provided with only the default partition and namespace
// config.ACLResolver - mock provided with ACLs disabled. Fills entMeta and authzContext with default partition and namespace
func RunResourceServiceWithConfig(t *testing.T, config svc.Config, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient {
t.Helper()

if config.Backend != nil {
panic("backend can not be configured")
}

backend, err := inmem.NewBackend()
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go backend.Run(ctx)
config.Backend = backend

if config.Registry == nil {
config.Registry = resource.NewRegistry()
}

registry := resource.NewRegistry()
for _, fn := range registerFns {
fn(registry)
fn(config.Registry)
}

server := grpc.NewServer()

mockTenancyBridge := &svc.MockTenancyBridge{}
mockTenancyBridge.On("PartitionExists", resource.DefaultPartitionName).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{
Backend: backend,
Registry: registry,
Logger: testutil.Logger(t),
ACLResolver: aclResolver,
TenancyBridge: mockTenancyBridge,
}).Register(server)
if config.TenancyBridge == nil {
mockTenancyBridge := &svc.MockTenancyBridge{}
mockTenancyBridge.On("PartitionExists", resource.DefaultPartitionName).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)
config.TenancyBridge = mockTenancyBridge
}

if config.ACLResolver == nil {
// Provide a resolver which will default partition and namespace when not provided. This is similar to user
// initiated requests.
//
// Controllers under test should be providing full tenancy since they will run with the DANGER_NO_AUTH.
mockACLResolver := &svc.MockACLResolver{}
mockACLResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything).
Return(testutils.ACLsDisabled(t), nil).
Run(func(args mock.Arguments) {
// Caller expecting passed in tokenEntMeta and authorizerContext to be filled in.
tokenEntMeta := args.Get(1).(*acl.EnterpriseMeta)
if tokenEntMeta != nil {
FillEntMeta(tokenEntMeta)
}

authzContext := args.Get(2).(*acl.AuthorizerContext)
if authzContext != nil {
FillAuthorizerContext(authzContext)
}
})
config.ACLResolver = mockACLResolver
}

if config.Logger == nil {
config.Logger = testutil.Logger(t)
}

svc.NewServer(config).Register(server)

pipe := internal.NewPipeListener()
go server.Serve(pipe)
Expand Down
49 changes: 46 additions & 3 deletions internal/resource/demo/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ import (
)

var (
// TypeV1Executive represents a a C-suite executive of the company.
// Used as a resource to test cluster scope.
TypeV1Executive = &pbresource.Type{
Group: "demo",
GroupVersion: "v1",
Kind: "Executive",
}

// TypeV1RecordLabel represents a record label which artists are signed to.
// Used specifically as a resource to test partition only scoped resources.
// Used as a resource to test partiion scope.
TypeV1RecordLabel = &pbresource.Type{
Group: "demo",
GroupVersion: "v1",
Expand Down Expand Up @@ -72,8 +80,12 @@ const (
ArtistV2ReadPolicy = `key_prefix "resource/demo.v2.Artist/" { policy = "read" }`
ArtistV2WritePolicy = `key_prefix "resource/demo.v2.Artist/" { policy = "write" }`
ArtistV2ListPolicy = `key_prefix "resource/" { policy = "list" }`
LabelV1ReadPolicy = `key_prefix "resource/demo.v1.Label/" { policy = "read" }`
LabelV1WritePolicy = `key_prefix "resource/demo.v1.Label/" { policy = "write" }`

ExecutiveV1ReadPolicy = `key_prefix "resource/demo.v1.Executive/" { policy = "read" }`
ExecutiveV1WritePolicy = `key_prefix "resource/demo.v1.Executive/" { policy = "write" }`

LabelV1ReadPolicy = `key_prefix "resource/demo.v1.Label/" { policy = "read" }`
LabelV1WritePolicy = `key_prefix "resource/demo.v1.Label/" { policy = "write" }`
)

// RegisterTypes registers the demo types. Should only be called in tests and
Expand Down Expand Up @@ -139,6 +151,17 @@ func RegisterTypes(r resource.Registry) {
return nil
}

r.Register(resource.Registration{
Type: TypeV1Executive,
Proto: &pbdemov1.Executive{},
Scope: resource.ScopeCluster,
ACLs: &resource.ACLHooks{
Read: readACL,
Write: writeACL,
List: makeListACL(TypeV1Executive),
},
})

r.Register(resource.Registration{
Type: TypeV1RecordLabel,
Proto: &pbdemov1.RecordLabel{},
Expand Down Expand Up @@ -209,6 +232,26 @@ func RegisterTypes(r resource.Registry) {
})
}

// GenerateV1Executive generates a named Executive resource.
func GenerateV1Executive(name, position string) (*pbresource.Resource, error) {
data, err := anypb.New(&pbdemov1.Executive{Position: position})
if err != nil {
return nil, err
}

return &pbresource.Resource{
Id: &pbresource.ID{
Type: TypeV1Executive,
Tenancy: resource.DefaultClusteredTenancy(),
Name: name,
},
Data: data,
Metadata: map[string]string{
"generated_at": time.Now().Format(time.RFC3339),
},
}, nil
}

// GenerateV1RecordLabel generates a named RecordLabel resource.
func GenerateV1RecordLabel(name string) (*pbresource.Resource, error) {
data, err := anypb.New(&pbdemov1.RecordLabel{Name: name})
Expand Down
8 changes: 4 additions & 4 deletions internal/resource/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestResourceWriteHandler(t *testing.T) {
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, demo.ArtistV1WritePolicy, demo.ArtistV2WritePolicy), nil)

client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
client := svctest.RunResourceServiceWithConfig(t, resourceSvc.Config{ACLResolver: aclResolver}, demo.RegisterTypes)

r := resource.NewRegistry()
demo.RegisterTypes(r)
Expand Down Expand Up @@ -359,7 +359,7 @@ func TestResourceReadHandler(t *testing.T) {
aclResolver.On("ResolveTokenAndDefaultMeta", fakeToken, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, ""), nil)

client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
client := svctest.RunResourceServiceWithConfig(t, resourceSvc.Config{ACLResolver: aclResolver}, demo.RegisterTypes)

r := resource.NewRegistry()
demo.RegisterTypes(r)
Expand Down Expand Up @@ -412,7 +412,7 @@ func TestResourceDeleteHandler(t *testing.T) {
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, demo.ArtistV2WritePolicy), nil)

client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
client := svctest.RunResourceServiceWithConfig(t, resourceSvc.Config{ACLResolver: aclResolver}, demo.RegisterTypes)

r := resource.NewRegistry()
demo.RegisterTypes(r)
Expand Down Expand Up @@ -489,7 +489,7 @@ func TestResourceListHandler(t *testing.T) {
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, demo.ArtistV2WritePolicy), nil)

client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
client := svctest.RunResourceServiceWithConfig(t, resourceSvc.Config{ACLResolver: aclResolver}, demo.RegisterTypes)

r := resource.NewRegistry()
demo.RegisterTypes(r)
Expand Down
10 changes: 10 additions & 0 deletions proto/private/pbdemo/v1/demo.pb.binary.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading