-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resource: List resources by owner (#17190)
- Loading branch information
Showing
13 changed files
with
688 additions
and
216 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package resource | ||
|
||
import ( | ||
"context" | ||
|
||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
|
||
"github.com/hashicorp/consul/acl" | ||
"github.com/hashicorp/consul/proto-public/pbresource" | ||
) | ||
|
||
func (s *Server) ListByOwner(ctx context.Context, req *pbresource.ListByOwnerRequest) (*pbresource.ListByOwnerResponse, error) { | ||
if err := validateListByOwnerRequest(req); err != nil { | ||
return nil, err | ||
} | ||
|
||
_, err := s.resolveType(req.Owner.Type) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
children, err := s.Backend.ListByOwner(ctx, req.Owner) | ||
if err != nil { | ||
return nil, status.Errorf(codes.Internal, "failed list by owner: %v", err) | ||
} | ||
|
||
authz, err := s.getAuthorizer(tokenFromContext(ctx)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result := make([]*pbresource.Resource, 0) | ||
for _, child := range children { | ||
reg, err := s.resolveType(child.Id.Type) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// ACL filter | ||
err = reg.ACLs.Read(authz, child.Id) | ||
switch { | ||
case acl.IsErrPermissionDenied(err): | ||
continue | ||
case err != nil: | ||
return nil, status.Errorf(codes.Internal, "failed read acl: %v", err) | ||
} | ||
|
||
result = append(result, child) | ||
} | ||
return &pbresource.ListByOwnerResponse{Resources: result}, nil | ||
} | ||
|
||
func validateListByOwnerRequest(req *pbresource.ListByOwnerRequest) error { | ||
if req.Owner == nil { | ||
return status.Errorf(codes.InvalidArgument, "owner is required") | ||
} | ||
|
||
if err := validateId(req.Owner, "owner"); err != nil { | ||
return err | ||
} | ||
|
||
if req.Owner.Uid == "" { | ||
return status.Errorf(codes.InvalidArgument, "owner uid is required") | ||
} | ||
return nil | ||
} |
174 changes: 174 additions & 0 deletions
174
agent/grpc-external/services/resource/list_by_owner_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
// // Copyright (c) HashiCorp, Inc. | ||
// // SPDX-License-Identifier: MPL-2.0 | ||
|
||
package resource | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/consul/acl" | ||
"github.com/hashicorp/consul/internal/resource/demo" | ||
"github.com/hashicorp/consul/proto-public/pbresource" | ||
"github.com/hashicorp/consul/proto/private/prototest" | ||
|
||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
func TestListByOwner_InputValidation(t *testing.T) { | ||
server := testServer(t) | ||
client := testClient(t, server) | ||
|
||
demo.RegisterTypes(server.Registry) | ||
|
||
testCases := map[string]func(*pbresource.ListByOwnerRequest){ | ||
"no owner": func(req *pbresource.ListByOwnerRequest) { req.Owner = nil }, | ||
"no type": func(req *pbresource.ListByOwnerRequest) { req.Owner.Type = nil }, | ||
"no tenancy": func(req *pbresource.ListByOwnerRequest) { req.Owner.Tenancy = nil }, | ||
"no name": func(req *pbresource.ListByOwnerRequest) { req.Owner.Name = "" }, | ||
"no uid": func(req *pbresource.ListByOwnerRequest) { req.Owner.Uid = "" }, | ||
// clone necessary to not pollute DefaultTenancy | ||
"tenancy partition not default": func(req *pbresource.ListByOwnerRequest) { | ||
req.Owner.Tenancy = clone(req.Owner.Tenancy) | ||
req.Owner.Tenancy.Partition = "" | ||
}, | ||
"tenancy namespace not default": func(req *pbresource.ListByOwnerRequest) { | ||
req.Owner.Tenancy = clone(req.Owner.Tenancy) | ||
req.Owner.Tenancy.Namespace = "" | ||
}, | ||
"tenancy peername not local": func(req *pbresource.ListByOwnerRequest) { | ||
req.Owner.Tenancy = clone(req.Owner.Tenancy) | ||
req.Owner.Tenancy.PeerName = "" | ||
}, | ||
} | ||
for desc, modFn := range testCases { | ||
t.Run(desc, func(t *testing.T) { | ||
res, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
req := &pbresource.ListByOwnerRequest{Owner: res.Id} | ||
modFn(req) | ||
|
||
_, err = client.ListByOwner(testContext(t), req) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
}) | ||
} | ||
} | ||
|
||
func TestListByOwner_TypeNotRegistered(t *testing.T) { | ||
server := testServer(t) | ||
client := testClient(t, server) | ||
|
||
_, err := client.ListByOwner(context.Background(), &pbresource.ListByOwnerRequest{ | ||
Owner: &pbresource.ID{ | ||
Type: demo.TypeV2Artist, | ||
Tenancy: demo.TenancyDefault, | ||
Uid: "bogus", | ||
Name: "bogus", | ||
}, | ||
}) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.Contains(t, err.Error(), "resource type demo.v2.artist not registered") | ||
} | ||
|
||
func TestListByOwner_Empty(t *testing.T) { | ||
server := testServer(t) | ||
demo.RegisterTypes(server.Registry) | ||
client := testClient(t, server) | ||
|
||
res, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) | ||
require.NoError(t, err) | ||
|
||
rsp2, err := client.ListByOwner(testContext(t), &pbresource.ListByOwnerRequest{Owner: rsp1.Resource.Id}) | ||
require.NoError(t, err) | ||
require.Empty(t, rsp2.Resources) | ||
} | ||
|
||
func TestListByOwner_Many(t *testing.T) { | ||
server := testServer(t) | ||
demo.RegisterTypes(server.Registry) | ||
client := testClient(t, server) | ||
|
||
res, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) | ||
artist := rsp1.Resource | ||
require.NoError(t, err) | ||
|
||
albums := make([]*pbresource.Resource, 10) | ||
for i := 0; i < len(albums); i++ { | ||
album, err := demo.GenerateV2Album(artist.Id) | ||
require.NoError(t, err) | ||
|
||
// Prevent test flakes if the generated names collide. | ||
album.Id.Name = fmt.Sprintf("%s-%d", artist.Id.Name, i) | ||
|
||
rsp2, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: album}) | ||
require.NoError(t, err) | ||
albums[i] = rsp2.Resource | ||
} | ||
|
||
rsp3, err := client.ListByOwner(testContext(t), &pbresource.ListByOwnerRequest{ | ||
Owner: artist.Id, | ||
}) | ||
require.NoError(t, err) | ||
prototest.AssertElementsMatch(t, albums, rsp3.Resources) | ||
} | ||
|
||
func TestListByOwner_ACL_PerTypeDenied(t *testing.T) { | ||
authz := AuthorizerFrom(t, `key_prefix "resource/demo.v2.album/" { policy = "deny" }`) | ||
_, rsp, err := roundTripListByOwner(t, authz) | ||
|
||
// verify resource filtered out, hence no results | ||
require.NoError(t, err) | ||
require.Empty(t, rsp.Resources) | ||
} | ||
|
||
func TestListByOwner_ACL_PerTypeAllowed(t *testing.T) { | ||
authz := AuthorizerFrom(t, `key_prefix "resource/demo.v2.album/" { policy = "read" }`) | ||
album, rsp, err := roundTripListByOwner(t, authz) | ||
|
||
// verify resource not filtered out | ||
require.NoError(t, err) | ||
require.Len(t, rsp.Resources, 1) | ||
prototest.AssertDeepEqual(t, album, rsp.Resources[0]) | ||
} | ||
|
||
// roundtrip a ListByOwner which attempts to return a single resource | ||
func roundTripListByOwner(t *testing.T, authz acl.Authorizer) (*pbresource.Resource, *pbresource.ListByOwnerResponse, error) { | ||
server := testServer(t) | ||
client := testClient(t, server) | ||
demo.RegisterTypes(server.Registry) | ||
|
||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: artist}) | ||
artist = rsp1.Resource | ||
require.NoError(t, err) | ||
|
||
album, err := demo.GenerateV2Album(artist.Id) | ||
require.NoError(t, err) | ||
|
||
rsp2, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: album}) | ||
album = rsp2.Resource | ||
require.NoError(t, err) | ||
|
||
mockACLResolver := &MockACLResolver{} | ||
mockACLResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). | ||
Return(authz, nil) | ||
server.ACLResolver = mockACLResolver | ||
|
||
rsp3, err := client.ListByOwner(testContext(t), &pbresource.ListByOwnerRequest{Owner: artist.Id}) | ||
return album, rsp3, err | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.