2024-01-25 19:12:30 +00:00
|
|
|
// 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) {
|
2024-02-20 20:42:31 +00:00
|
|
|
tenancyMarkedForDeletion, err := s.mutateAndValidate(ctx, req.Resource, false)
|
2024-01-25 19:12:30 +00:00
|
|
|
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.
|
2024-02-20 20:42:31 +00:00
|
|
|
func (s *Server) mutateAndValidate(ctx context.Context, res *pbresource.Resource, enforceLicenseCheck bool) (tenancyMarkedForDeletion bool, err error) {
|
|
|
|
reg, err := s.ensureResourceValid(res, enforceLicenseCheck)
|
2024-01-25 19:12:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-02-20 20:42:31 +00:00
|
|
|
func (s *Server) ensureResourceValid(res *pbresource.Resource, enforceLicenseCheck bool) (*resource.Registration, error) {
|
2024-01-25 19:12:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-02-20 20:42:31 +00:00
|
|
|
// Since this is shared by Write and MutateAndValidate, only fail the operation
|
|
|
|
// if it's a write operation and the feature is not allowed by the license.
|
|
|
|
if err = s.FeatureCheck(reg); err != nil && enforceLicenseCheck {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-25 19:12:30 +00:00
|
|
|
// 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
|
|
|
|
}
|