Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource: Make resource watchlist tenancy aware #18539

Merged
merged 1 commit into from
Aug 21, 2023
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
78 changes: 1 addition & 77 deletions agent/grpc-external/services/resource/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,83 +119,7 @@ func TestList_Many(t *testing.T) {
func TestList_Tenancy_Defaults_And_Normalization(t *testing.T) {
// Test units of tenancy get defaulted correctly when empty.
ctx := context.Background()
testCases := map[string]struct {
typ *pbresource.Type
tenancy *pbresource.Tenancy
}{
"namespaced type with empty partition": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: resource.DefaultNamespaceName,
PeerName: "local",
},
},
"namespaced type with empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with uppercase partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "DEFAULT",
PeerName: "local",
},
},
"namespaced type with wildcard partition and empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "*",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and wildcard namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "*",
PeerName: "local",
},
},
"partitioned type with empty partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with uppercase partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with wildcard partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "*",
PeerName: "local",
},
},
}
for desc, tc := range testCases {
for desc, tc := range wildcardTenancyCases() {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
demo.RegisterTypes(server.Registry)
Expand Down
85 changes: 85 additions & 0 deletions agent/grpc-external/services/resource/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hashicorp/consul/agent/grpc-external/testutils"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/storage/inmem"
"github.com/hashicorp/consul/proto-public/pbresource"
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2"
Expand Down Expand Up @@ -131,6 +132,90 @@ func modifyArtist(t *testing.T, res *pbresource.Resource) *pbresource.Resource {
return res
}

// wildcardTenancyCases returns permutations of tenancy and type scope used as input
// to endpoints that accept wildcards for tenancy.
func wildcardTenancyCases() map[string]struct {
typ *pbresource.Type
tenancy *pbresource.Tenancy
} {
return map[string]struct {
typ *pbresource.Type
tenancy *pbresource.Tenancy
}{
"namespaced type with empty partition": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: resource.DefaultNamespaceName,
PeerName: "local",
},
},
"namespaced type with empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with uppercase partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "DEFAULT",
PeerName: "local",
},
},
"namespaced type with wildcard partition and empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "*",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and wildcard namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "*",
PeerName: "local",
},
},
"partitioned type with empty partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with uppercase partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with wildcard partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "*",
PeerName: "local",
},
},
}
}

// tenancyCases returns permutations of valid tenancy structs in a resource id to use as inputs.
// - the id is for a recordLabel when the resource is partition scoped
// - the id is for an artist when the resource is namespace scoped
Expand Down
58 changes: 45 additions & 13 deletions agent/grpc-external/services/resource/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,26 @@ import (
"google.golang.org/grpc/status"

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/storage"
"github.com/hashicorp/consul/proto-public/pbresource"
)

func (s *Server) WatchList(req *pbresource.WatchListRequest, stream pbresource.ResourceService_WatchListServer) error {
if err := validateWatchListRequest(req); err != nil {
return err
}

// check type exists
reg, err := s.resolveType(req.Type)
reg, err := s.validateWatchListRequest(req)
if err != nil {
return err
}

// TODO(spatel): Refactor _ and entMeta as part of NET-4914
authz, authzContext, err := s.getAuthorizer(tokenFromContext(stream.Context()), acl.DefaultEnterpriseMeta())
// v1 ACL subsystem is "wildcard" aware so just pass on through.
entMeta := v2TenancyToV1EntMeta(req.Tenancy)
token := tokenFromContext(stream.Context())
authz, authzContext, err := s.getAuthorizer(token, entMeta)
if err != nil {
return err
}

// check acls
// Check list ACL.
err = reg.ACLs.List(authz, authzContext)
switch {
case acl.IsErrPermissionDenied(err):
Expand All @@ -40,6 +38,9 @@ func (s *Server) WatchList(req *pbresource.WatchListRequest, stream pbresource.R
return status.Errorf(codes.Internal, "failed list acl: %v", err)
}

// Ensure we're defaulting correctly when request tenancy units are empty.
v1EntMetaToV2Tenancy(reg, entMeta, req.Tenancy)

unversionedType := storage.UnversionedTypeFrom(req.Type)
watch, err := s.Backend.WatchList(
stream.Context(),
Expand All @@ -66,6 +67,15 @@ func (s *Server) WatchList(req *pbresource.WatchListRequest, stream pbresource.R
continue
}

// Need to rebuild authorizer per resource since wildcard inputs may
// result in different tenancies. Consider caching per tenancy if this
// is deemed expensive.
entMeta = v2TenancyToV1EntMeta(event.Resource.Id.Tenancy)
authz, authzContext, err = s.getAuthorizer(token, entMeta)
if err != nil {
return err
}

// filter out items that don't pass read ACLs
err = reg.ACLs.Read(authz, authzContext, event.Resource.Id)
switch {
Expand All @@ -81,15 +91,37 @@ func (s *Server) WatchList(req *pbresource.WatchListRequest, stream pbresource.R
}
}

func validateWatchListRequest(req *pbresource.WatchListRequest) error {
func (s *Server) validateWatchListRequest(req *pbresource.WatchListRequest) (*resource.Registration, error) {
var field string
switch {
case req.Type == nil:
field = "type"
case req.Tenancy == nil:
field = "tenancy"
default:
return nil
}
return status.Errorf(codes.InvalidArgument, "%s is required", field)

if field != "" {
return nil, status.Errorf(codes.InvalidArgument, "%s is required", field)
}

// Check type exists.
reg, err := s.resolveType(req.Type)
if err != nil {
return nil, err
}

// Lowercase
resource.Normalize(req.Tenancy)

// Error when partition scoped and namespace not empty.
if reg.Scope == resource.ScopePartition && req.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"partition scoped type %s cannot have a namespace. got: %s",
resource.ToGVK(req.Type),
req.Tenancy.Namespace,
)
}

return reg, nil
}
Loading
Loading