consul/agent/grpc-external/services/resource/mutate_and_validate.go

140 lines
3.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"context"
"strings"
"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/proto-public/pbresource"
)
func (s *Server) MutateAndValidate(ctx context.Context, req *pbresource.MutateAndValidateRequest) (*pbresource.MutateAndValidateResponse, error) {
tenancyMarkedForDeletion, err := s.mutateAndValidate(ctx, req.Resource)
if err != nil {
return nil, err
}
if tenancyMarkedForDeletion {
return nil, status.Errorf(
codes.InvalidArgument,
"tenancy marked for deletion: %s/%s",
req.Resource.Id.Tenancy.Partition,
req.Resource.Id.Tenancy.Namespace,
)
}
return &pbresource.MutateAndValidateResponse{Resource: req.Resource}, nil
}
// private DRY impl that is used by both the Write and MutateAndValidate RPCs.
func (s *Server) mutateAndValidate(ctx context.Context, res *pbresource.Resource) (tenancyMarkedForDeletion bool, err error) {
reg, err := s.ensureResourceValid(res)
if err != nil {
return false, err
}
v1EntMeta := v2TenancyToV1EntMeta(res.Id.Tenancy)
authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), v1EntMeta)
if err != nil {
return false, err
}
v1EntMetaToV2Tenancy(reg, v1EntMeta, res.Id.Tenancy)
// Check the user sent the correct type of data.
if res.Data != nil && !res.Data.MessageIs(reg.Proto) {
got := strings.TrimPrefix(res.Data.TypeUrl, "type.googleapis.com/")
return false, status.Errorf(
codes.InvalidArgument,
"resource.data is of wrong type (expected=%q, got=%q)",
reg.Proto.ProtoReflect().Descriptor().FullName(),
got,
)
}
if err = reg.Mutate(res); err != nil {
return false, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error())
}
if err = reg.Validate(res); err != nil {
return false, status.Error(codes.InvalidArgument, err.Error())
}
// ACL check comes before tenancy existence checks to not leak tenancy "existence".
err = reg.ACLs.Write(authz, authzContext, res)
switch {
case acl.IsErrPermissionDenied(err):
return false, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
return false, status.Errorf(codes.Internal, "failed write acl: %v", err)
}
// Check tenancy exists for the V2 resource
if err = tenancyExists(reg, s.TenancyBridge, res.Id.Tenancy, codes.InvalidArgument); err != nil {
return false, err
}
// This is used later in the "create" and "update" paths to block non-delete related writes
// when a tenancy unit has been marked for deletion.
tenancyMarkedForDeletion, err = isTenancyMarkedForDeletion(reg, s.TenancyBridge, res.Id.Tenancy)
if err != nil {
return false, status.Errorf(codes.Internal, "failed tenancy marked for deletion check: %v", err)
}
if tenancyMarkedForDeletion {
return true, nil
}
return false, nil
}
func (s *Server) ensureResourceValid(res *pbresource.Resource) (*resource.Registration, error) {
var field string
switch {
case res == nil:
field = "resource"
case res.Id == nil:
field = "resource.id"
}
if field != "" {
return nil, status.Errorf(codes.InvalidArgument, "%s is required", field)
}
if err := validateId(res.Id, "resource.id"); err != nil {
return nil, err
}
if res.Owner != nil {
if err := validateId(res.Owner, "resource.owner"); err != nil {
return nil, err
}
}
// Check type exists.
reg, err := s.resolveType(res.Id.Type)
if err != nil {
return nil, err
}
if err = checkV2Tenancy(s.UseV2Tenancy, res.Id.Type); err != nil {
return nil, err
}
// Check scope
if reg.Scope == resource.ScopePartition && res.Id.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"partition scoped resource %s cannot have a namespace. got: %s",
resource.ToGVK(res.Id.Type),
res.Id.Tenancy.Namespace,
)
}
return reg, nil
}