2023-03-28 18:39:22 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-28 18:39:22 +00:00
|
|
|
|
2023-03-09 19:40:23 +00:00
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-10-16 17:55:30 +00:00
|
|
|
"strings"
|
2023-03-09 19:40:23 +00:00
|
|
|
|
2023-04-06 09:40:04 +00:00
|
|
|
"github.com/hashicorp/go-hclog"
|
2023-03-09 19:40:23 +00:00
|
|
|
"google.golang.org/grpc"
|
2023-03-27 19:37:54 +00:00
|
|
|
"google.golang.org/grpc/codes"
|
2023-03-27 21:25:27 +00:00
|
|
|
"google.golang.org/grpc/metadata"
|
2023-03-27 19:37:54 +00:00
|
|
|
"google.golang.org/grpc/status"
|
2023-04-06 09:40:04 +00:00
|
|
|
"google.golang.org/protobuf/proto"
|
2023-03-09 19:40:23 +00:00
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
"github.com/hashicorp/consul/acl"
|
|
|
|
"github.com/hashicorp/consul/acl/resolver"
|
2023-03-27 15:35:39 +00:00
|
|
|
"github.com/hashicorp/consul/internal/resource"
|
|
|
|
"github.com/hashicorp/consul/internal/storage"
|
2023-03-09 19:40:23 +00:00
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Server struct {
|
|
|
|
Config
|
|
|
|
}
|
|
|
|
|
|
|
|
type Config struct {
|
2023-04-06 09:40:04 +00:00
|
|
|
Logger hclog.Logger
|
|
|
|
Registry Registry
|
2023-04-04 16:30:06 +00:00
|
|
|
|
|
|
|
// Backend is the storage backend that will be used for resource persistence.
|
2023-04-11 11:10:14 +00:00
|
|
|
Backend Backend
|
|
|
|
ACLResolver ACLResolver
|
2023-09-18 16:25:05 +00:00
|
|
|
// TenancyBridge temporarily allows us to use V1 implementations of
|
2023-08-07 21:37:03 +00:00
|
|
|
// partitions and namespaces until V2 implementations are available.
|
2023-09-18 16:25:05 +00:00
|
|
|
TenancyBridge TenancyBridge
|
2023-03-27 19:37:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//go:generate mockery --name Registry --inpackage
|
|
|
|
type Registry interface {
|
|
|
|
resource.Registry
|
2023-03-27 15:35:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//go:generate mockery --name Backend --inpackage
|
|
|
|
type Backend interface {
|
|
|
|
storage.Backend
|
2023-03-09 19:40:23 +00:00
|
|
|
}
|
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
//go:generate mockery --name ACLResolver --inpackage
|
|
|
|
type ACLResolver interface {
|
|
|
|
ResolveTokenAndDefaultMeta(string, *acl.EnterpriseMeta, *acl.AuthorizerContext) (resolver.Result, error)
|
|
|
|
}
|
|
|
|
|
2023-08-07 21:37:03 +00:00
|
|
|
//go:generate mockery --name TenancyBridge --inpackage
|
|
|
|
type TenancyBridge interface {
|
|
|
|
PartitionExists(partition string) (bool, error)
|
2023-08-10 14:53:38 +00:00
|
|
|
IsPartitionMarkedForDeletion(partition string) (bool, error)
|
|
|
|
NamespaceExists(partition, namespace string) (bool, error)
|
|
|
|
IsNamespaceMarkedForDeletion(partition, namespace string) (bool, error)
|
2023-08-07 21:37:03 +00:00
|
|
|
}
|
|
|
|
|
2023-03-09 19:40:23 +00:00
|
|
|
func NewServer(cfg Config) *Server {
|
|
|
|
return &Server{cfg}
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ pbresource.ResourceServiceServer = (*Server)(nil)
|
|
|
|
|
|
|
|
func (s *Server) Register(grpcServer *grpc.Server) {
|
|
|
|
pbresource.RegisterResourceServiceServer(grpcServer, s)
|
|
|
|
}
|
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
// Get token from grpc metadata or AnonymounsTokenId if not found
|
|
|
|
func tokenFromContext(ctx context.Context) string {
|
|
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return acl.AnonymousTokenID
|
|
|
|
}
|
|
|
|
|
|
|
|
vals := md.Get("x-consul-token")
|
|
|
|
if len(vals) == 0 {
|
|
|
|
return acl.AnonymousTokenID
|
|
|
|
}
|
|
|
|
return vals[0]
|
|
|
|
}
|
|
|
|
|
2023-03-27 19:37:54 +00:00
|
|
|
func (s *Server) resolveType(typ *pbresource.Type) (*resource.Registration, error) {
|
2023-04-06 09:40:04 +00:00
|
|
|
v, ok := s.Registry.Resolve(typ)
|
2023-03-27 19:37:54 +00:00
|
|
|
if ok {
|
|
|
|
return &v, nil
|
|
|
|
}
|
|
|
|
return nil, status.Errorf(
|
|
|
|
codes.InvalidArgument,
|
|
|
|
"resource type %s not registered", resource.ToGVK(typ),
|
|
|
|
)
|
2023-03-09 19:40:23 +00:00
|
|
|
}
|
2023-03-27 21:25:27 +00:00
|
|
|
|
|
|
|
func readConsistencyFrom(ctx context.Context) storage.ReadConsistency {
|
|
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return storage.EventualConsistency
|
|
|
|
}
|
|
|
|
|
|
|
|
vals := md.Get("x-consul-consistency-mode")
|
|
|
|
if len(vals) == 0 {
|
|
|
|
return storage.EventualConsistency
|
|
|
|
}
|
|
|
|
|
|
|
|
if vals[0] == "consistent" {
|
|
|
|
return storage.StrongConsistency
|
|
|
|
}
|
|
|
|
return storage.EventualConsistency
|
|
|
|
}
|
2023-04-06 09:40:04 +00:00
|
|
|
|
2023-08-07 21:37:03 +00:00
|
|
|
func (s *Server) getAuthorizer(token string, entMeta *acl.EnterpriseMeta) (acl.Authorizer, *acl.AuthorizerContext, error) {
|
|
|
|
authzContext := &acl.AuthorizerContext{}
|
|
|
|
authz, err := s.ACLResolver.ResolveTokenAndDefaultMeta(token, entMeta, authzContext)
|
2023-04-11 11:10:14 +00:00
|
|
|
if err != nil {
|
2023-08-07 21:37:03 +00:00
|
|
|
return nil, nil, status.Errorf(codes.Internal, "failed getting authorizer: %v", err)
|
2023-04-11 11:10:14 +00:00
|
|
|
}
|
2023-08-07 21:37:03 +00:00
|
|
|
return authz, authzContext, nil
|
2023-04-11 11:10:14 +00:00
|
|
|
}
|
|
|
|
|
2023-04-14 13:19:46 +00:00
|
|
|
func isGRPCStatusError(err error) bool {
|
|
|
|
if err == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
_, ok := status.FromError(err)
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2023-04-17 21:33:20 +00:00
|
|
|
func validateId(id *pbresource.ID, errorPrefix string) error {
|
2023-10-16 17:55:30 +00:00
|
|
|
if id.Type == nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.type is required", errorPrefix)
|
2023-04-17 21:33:20 +00:00
|
|
|
}
|
|
|
|
|
2023-10-16 17:55:30 +00:00
|
|
|
if err := resource.ValidateName(id.Name); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.name invalid: %v", errorPrefix, err)
|
2023-04-17 21:33:20 +00:00
|
|
|
}
|
2023-08-31 14:24:09 +00:00
|
|
|
|
|
|
|
// Better UX: Allow callers to pass in nil tenancy. Defaulting and inheritance of tenancy
|
|
|
|
// from the request token will take place further down in the call flow.
|
|
|
|
if id.Tenancy == nil {
|
|
|
|
id.Tenancy = &pbresource.Tenancy{
|
|
|
|
Partition: "",
|
|
|
|
Namespace: "",
|
2023-09-15 14:34:18 +00:00
|
|
|
// TODO(spatel): NET-5475 - Remove as part of peer_name moving to PeerTenancy
|
2023-08-31 14:24:09 +00:00
|
|
|
PeerName: "local",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-16 17:55:30 +00:00
|
|
|
if id.Tenancy.Partition != "" {
|
|
|
|
if err := resource.ValidateName(id.Tenancy.Partition); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.tenancy.partition invalid: %v", errorPrefix, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if id.Tenancy.Namespace != "" {
|
|
|
|
if err := resource.ValidateName(id.Tenancy.Namespace); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.tenancy.namespace invalid: %v", errorPrefix, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO(spatel): NET-5475 - Remove as part of peer_name moving to PeerTenancy
|
|
|
|
if id.Tenancy.PeerName == "" {
|
|
|
|
id.Tenancy.PeerName = resource.DefaultPeerName
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateRef(ref *pbresource.Reference, errorPrefix string) error {
|
|
|
|
if ref.Type == nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.type is required", errorPrefix)
|
|
|
|
}
|
|
|
|
if err := resource.ValidateName(ref.Name); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.name invalid: %v", errorPrefix, err)
|
|
|
|
}
|
|
|
|
if err := resource.ValidateName(ref.Tenancy.Partition); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.tenancy.partition invalid: %v", errorPrefix, err)
|
|
|
|
}
|
|
|
|
if err := resource.ValidateName(ref.Tenancy.Namespace); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "%s.tenancy.namespace invalid: %v", errorPrefix, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateWildcardTenancy(tenancy *pbresource.Tenancy, namePrefix string) error {
|
|
|
|
// Partition has to be a valid name if not wildcard or empty
|
|
|
|
if tenancy.Partition != "" && tenancy.Partition != "*" {
|
|
|
|
if err := resource.ValidateName(tenancy.Partition); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "tenancy.partition invalid: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Namespace has to be a valid name if not wildcard or empty
|
|
|
|
if tenancy.Namespace != "" && tenancy.Namespace != "*" {
|
|
|
|
if err := resource.ValidateName(tenancy.Namespace); err != nil {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "tenancy.namespace invalid: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not doing a strict resource name validation here because the prefix can be
|
|
|
|
// something like "foo-" which is a valid prefix but not valid resource name.
|
|
|
|
// relax validation to just check for lowercasing
|
|
|
|
if namePrefix != strings.ToLower(namePrefix) {
|
|
|
|
return status.Errorf(codes.InvalidArgument, "name_prefix invalid: must be lowercase alphanumeric, got: %v", namePrefix)
|
|
|
|
}
|
2023-04-17 21:33:20 +00:00
|
|
|
|
2023-10-23 21:30:47 +00:00
|
|
|
// TODO(spatel): NET-5475 - Remove as part of peer_name moving to PeerTenancy
|
|
|
|
if tenancy.PeerName == "" {
|
|
|
|
tenancy.PeerName = resource.DefaultPeerName
|
|
|
|
}
|
|
|
|
|
2023-08-07 21:37:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-20 18:49:54 +00:00
|
|
|
// tenancyExists return an error with the passed in gRPC status code when tenancy partition or namespace do not exist.
|
|
|
|
func tenancyExists(reg *resource.Registration, tenancyBridge TenancyBridge, tenancy *pbresource.Tenancy, errCode codes.Code) error {
|
2023-08-07 21:37:03 +00:00
|
|
|
if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
|
2023-10-20 18:49:54 +00:00
|
|
|
exists, err := tenancyBridge.PartitionExists(tenancy.Partition)
|
2023-08-07 21:37:03 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
|
|
|
return err
|
|
|
|
case !exists:
|
2023-10-16 17:55:30 +00:00
|
|
|
return status.Errorf(errCode, "partition not found: %v", tenancy.Partition)
|
2023-08-07 21:37:03 +00:00
|
|
|
}
|
2023-04-17 21:33:20 +00:00
|
|
|
}
|
|
|
|
|
2023-08-07 21:37:03 +00:00
|
|
|
if reg.Scope == resource.ScopeNamespace {
|
2023-10-20 18:49:54 +00:00
|
|
|
exists, err := tenancyBridge.NamespaceExists(tenancy.Partition, tenancy.Namespace)
|
2023-08-07 21:37:03 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
|
|
|
return err
|
|
|
|
case !exists:
|
2023-10-16 17:55:30 +00:00
|
|
|
return status.Errorf(errCode, "namespace not found: %v", tenancy.Namespace)
|
2023-08-10 14:53:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-20 18:49:54 +00:00
|
|
|
// tenancyMarkedForDeletion returns a gRPC InvalidArgument when either partition or namespace is marked for deletion.
|
|
|
|
func tenancyMarkedForDeletion(reg *resource.Registration, tenancyBridge TenancyBridge, tenancy *pbresource.Tenancy) error {
|
2023-08-10 14:53:38 +00:00
|
|
|
if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
|
2023-10-20 18:49:54 +00:00
|
|
|
marked, err := tenancyBridge.IsPartitionMarkedForDeletion(tenancy.Partition)
|
2023-08-10 14:53:38 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
|
|
|
return err
|
|
|
|
case marked:
|
|
|
|
return status.Errorf(codes.InvalidArgument, "partition marked for deletion: %v", tenancy.Partition)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if reg.Scope == resource.ScopeNamespace {
|
2023-10-20 18:49:54 +00:00
|
|
|
marked, err := tenancyBridge.IsNamespaceMarkedForDeletion(tenancy.Partition, tenancy.Namespace)
|
2023-08-10 14:53:38 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
|
|
|
return err
|
|
|
|
case marked:
|
|
|
|
return status.Errorf(codes.InvalidArgument, "namespace marked for deletion: %v", tenancy.Namespace)
|
2023-08-07 21:37:03 +00:00
|
|
|
}
|
2023-04-17 21:33:20 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-06 09:40:04 +00:00
|
|
|
func clone[T proto.Message](v T) T { return proto.Clone(v).(T) }
|