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

117 lines
3.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"context"
"errors"
"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) Read(ctx context.Context, req *pbresource.ReadRequest) (*pbresource.ReadResponse, error) {
// Light first pass validation based on what user passed in and not much more.
reg, err := s.validateReadRequest(req)
if err != nil {
return nil, err
}
// acl.EnterpriseMeta acl.AuthorizerContext follow rules for V1 resources since they integrate with the V1 acl subsystem.
// pbresource.Tenacy follows rules for V2 resources and the Resource service.
// Example:
//
// A CE namespace scoped resource:
// V1: EnterpriseMeta{}
// V2: Tenancy {Partition: "default", Namespace: "default"}
//
// An ENT namespace scoped resource:
// V1: EnterpriseMeta{Partition: "default", Namespace: "default"}
// V2: Tenancy {Partition: "default", Namespace: "default"}
//
// It is necessary to convert back and forth depending on which component supports which version, V1 or V2.
entMeta := v2TenancyToV1EntMeta(req.Id.Tenancy)
authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), entMeta)
if err != nil {
return nil, err
}
v1EntMetaToV2Tenancy(reg, entMeta, req.Id.Tenancy)
// ACL check usually comes before tenancy existence checks to not leak
// tenancy "existence", unless the ACL check requires the data payload
// to function.
authzNeedsData := false
err = reg.ACLs.Read(authz, authzContext, req.Id, nil)
switch {
case errors.Is(err, resource.ErrNeedData):
authzNeedsData = true
err = nil
case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
return nil, status.Errorf(codes.Internal, "failed read acl: %v", err)
}
// Check V1 tenancy exists for the V2 resource.
if err = v1TenancyExists(reg, s.TenancyBridge, req.Id.Tenancy, codes.NotFound); err != nil {
return nil, err
}
resource, err := s.Backend.Read(ctx, readConsistencyFrom(ctx), req.Id)
switch {
case errors.Is(err, storage.ErrNotFound):
return nil, status.Error(codes.NotFound, err.Error())
case errors.As(err, &storage.GroupVersionMismatchError{}):
return nil, status.Error(codes.InvalidArgument, err.Error())
case err != nil:
return nil, status.Errorf(codes.Internal, "failed read: %v", err)
}
if authzNeedsData {
err = reg.ACLs.Read(authz, authzContext, req.Id, resource)
switch {
case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
return nil, status.Errorf(codes.Internal, "failed read acl: %v", err)
}
}
return &pbresource.ReadResponse{Resource: resource}, nil
}
func (s *Server) validateReadRequest(req *pbresource.ReadRequest) (*resource.Registration, error) {
if req.Id == nil {
return nil, status.Errorf(codes.InvalidArgument, "id is required")
}
if err := validateId(req.Id, "id"); err != nil {
return nil, err
}
// Check type exists.
reg, err := s.resolveType(req.Id.Type)
if err != nil {
return nil, err
}
// Check scope
if reg.Scope == resource.ScopePartition && req.Id.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"partition scoped resource %s cannot have a namespace. got: %s",
resource.ToGVK(req.Id.Type),
req.Id.Tenancy.Namespace,
)
}
return reg, nil
}