R.B. Boyer ef6f2494c7
resource: allow for the ACLs.Read hook to request the entire data payload to perform the authz check (#18925)
The ACLs.Read hook for a resource only allows for the identity of a 
resource to be passed in for use in authz consideration. For some 
resources we wish to allow for the current stored value to dictate how 
to enforce the ACLs (such as reading a list of applicable services from 
the payload and allowing service:read on any of them to control reading the enclosing resource).

This change update the interface to usually accept a *pbresource.ID, 
but if the hook decides it needs more data it returns a sentinel error 
and the resource service knows to defer the authz check until after
 fetching the data from storage.
2023-09-22 09:53:55 -05:00

118 lines
3.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"context"
"google.golang.org/grpc/codes"
"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) List(ctx context.Context, req *pbresource.ListRequest) (*pbresource.ListResponse, error) {
reg, err := s.validateListRequest(req)
if err != nil {
return nil, err
}
// v1 ACL subsystem is "wildcard" aware so just pass on through.
entMeta := v2TenancyToV1EntMeta(req.Tenancy)
token := tokenFromContext(ctx)
authz, authzContext, err := s.getAuthorizer(token, entMeta)
if err != nil {
return nil, err
}
// Check ACLs.
err = reg.ACLs.List(authz, authzContext)
switch {
case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
return nil, 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)
resources, err := s.Backend.List(
ctx,
readConsistencyFrom(ctx),
storage.UnversionedTypeFrom(req.Type),
req.Tenancy,
req.NamePrefix,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed list: %v", err)
}
result := make([]*pbresource.Resource, 0)
for _, resource := range resources {
// Filter out non-matching GroupVersion.
if resource.Id.Type.GroupVersion != req.Type.GroupVersion {
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(resource.Id.Tenancy)
authz, authzContext, err = s.getAuthorizer(token, entMeta)
if err != nil {
return nil, err
}
// Filter out items that don't pass read ACLs.
err = reg.ACLs.Read(authz, authzContext, resource.Id, resource)
switch {
case acl.IsErrPermissionDenied(err):
continue
case err != nil:
return nil, status.Errorf(codes.Internal, "failed read acl: %v", err)
}
result = append(result, resource)
}
return &pbresource.ListResponse{Resources: result}, nil
}
func (s *Server) validateListRequest(req *pbresource.ListRequest) (*resource.Registration, error) {
var field string
switch {
case req.Type == nil:
field = "type"
case req.Tenancy == nil:
field = "tenancy"
}
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
}