2023-03-28 22:48:58 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-28 22:48:58 +00:00
|
|
|
|
2023-03-14 18:30:25 +00:00
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
2023-09-22 14:53:55 +00:00
|
|
|
"errors"
|
2023-03-14 18:30:25 +00:00
|
|
|
"fmt"
|
2023-06-26 12:25:14 +00:00
|
|
|
"regexp"
|
2023-08-11 19:52:51 +00:00
|
|
|
"strings"
|
2023-03-14 18:30:25 +00:00
|
|
|
"sync"
|
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
"github.com/hashicorp/consul/acl"
|
2023-09-01 14:44:53 +00:00
|
|
|
"github.com/hashicorp/consul/internal/storage"
|
2023-03-14 18:30:25 +00:00
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
)
|
|
|
|
|
2023-06-26 12:25:14 +00:00
|
|
|
var (
|
|
|
|
groupRegexp = regexp.MustCompile(`^[a-z][a-z\d_]+$`)
|
|
|
|
groupVersionRegexp = regexp.MustCompile(`^v([a-z\d]+)?\d$`)
|
|
|
|
kindRegexp = regexp.MustCompile(`^[A-Z][A-Za-z\d]+$`)
|
2023-09-01 14:44:53 +00:00
|
|
|
// Track resource types that are allowed to have an undefined scope. These are usually
|
|
|
|
// non-customer facing or internal types.
|
|
|
|
undefinedScopeAllowed = map[string]bool{
|
|
|
|
storage.UnversionedTypeFrom(TypeV1Tombstone).String(): true,
|
|
|
|
}
|
2023-06-26 12:25:14 +00:00
|
|
|
)
|
|
|
|
|
2023-09-01 14:44:53 +00:00
|
|
|
func isUndefinedScopeAllowed(t *pbresource.Type) bool {
|
|
|
|
return undefinedScopeAllowed[storage.UnversionedTypeFrom(t).String()]
|
|
|
|
}
|
|
|
|
|
2023-03-14 18:30:25 +00:00
|
|
|
type Registry interface {
|
|
|
|
// Register the given resource type and its hooks.
|
|
|
|
Register(reg Registration)
|
|
|
|
|
|
|
|
// Resolve the given resource type and its hooks.
|
|
|
|
Resolve(typ *pbresource.Type) (reg Registration, ok bool)
|
2023-08-04 18:27:48 +00:00
|
|
|
|
|
|
|
Types() []Registration
|
2023-03-14 18:30:25 +00:00
|
|
|
}
|
|
|
|
|
2023-10-26 20:39:06 +00:00
|
|
|
// ValidationHook is the function signature for a validation hook. These hooks can inspect
|
|
|
|
// the data as they see fit but are expected to not mutate the data in any way. If Go
|
|
|
|
// supported it, we would pass something akin to a const pointer into the callback to have
|
|
|
|
// the compiler enforce this immutability.
|
|
|
|
type ValidationHook func(*pbresource.Resource) error
|
|
|
|
|
|
|
|
// MutationHook is the function signature for a validation hook. These hooks can inspect
|
|
|
|
// and mutate the resource. If modifying the resources Data, the hook needs to ensure that
|
|
|
|
// the data gets reencoded and stored back to the Data field.
|
|
|
|
type MutationHook func(*pbresource.Resource) error
|
|
|
|
|
2023-10-13 23:16:26 +00:00
|
|
|
var ErrNeedResource = errors.New("authorization check requires the entire resource")
|
2023-09-22 14:53:55 +00:00
|
|
|
|
2023-10-26 20:39:06 +00:00
|
|
|
type ACLAuthorizeReadHook func(acl.Authorizer, *acl.AuthorizerContext, *pbresource.ID, *pbresource.Resource) error
|
|
|
|
type ACLAuthorizeWriteHook func(acl.Authorizer, *acl.AuthorizerContext, *pbresource.Resource) error
|
|
|
|
type ACLAuthorizeListHook func(acl.Authorizer, *acl.AuthorizerContext) error
|
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
type ACLHooks struct {
|
|
|
|
// Read is used to authorize Read RPCs and to filter results in List
|
|
|
|
// RPCs.
|
|
|
|
//
|
2023-09-22 14:53:55 +00:00
|
|
|
// It can be called an ID and possibly a Resource. The check will first
|
2023-10-13 23:16:26 +00:00
|
|
|
// attempt to use the ID and if the hook returns ErrNeedResource, then the
|
2023-09-22 14:53:55 +00:00
|
|
|
// check will be deferred until the data is fetched from the storage layer.
|
|
|
|
//
|
2023-04-11 11:10:14 +00:00
|
|
|
// If it is omitted, `operator:read` permission is assumed.
|
2023-10-26 20:39:06 +00:00
|
|
|
Read ACLAuthorizeReadHook
|
2023-04-11 11:10:14 +00:00
|
|
|
|
|
|
|
// Write is used to authorize Write and Delete RPCs.
|
|
|
|
//
|
|
|
|
// If it is omitted, `operator:write` permission is assumed.
|
2023-10-26 20:39:06 +00:00
|
|
|
Write ACLAuthorizeWriteHook
|
2023-04-11 11:10:14 +00:00
|
|
|
|
|
|
|
// List is used to authorize List RPCs.
|
|
|
|
//
|
|
|
|
// If it is omitted, we only filter the results using Read.
|
2023-10-26 20:39:06 +00:00
|
|
|
List ACLAuthorizeListHook
|
2023-04-11 11:10:14 +00:00
|
|
|
}
|
2023-03-14 18:30:25 +00:00
|
|
|
|
|
|
|
// Resource type registry
|
|
|
|
type TypeRegistry struct {
|
|
|
|
// registrations keyed by GVK
|
|
|
|
registrations map[string]Registration
|
|
|
|
lock sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewRegistry() Registry {
|
2023-04-28 15:49:08 +00:00
|
|
|
registry := &TypeRegistry{registrations: make(map[string]Registration)}
|
|
|
|
// Tombstone is an implicitly registered type since it is used to implement
|
|
|
|
// the cascading deletion of resources. ACLs end up being defaulted to
|
|
|
|
// operator:<read,write>. It is useful to note that tombstone creation
|
|
|
|
// does not get routed through the resource service and bypasses ACLs
|
|
|
|
// as part of the Delete endpoint.
|
|
|
|
registry.Register(Registration{
|
|
|
|
Type: TypeV1Tombstone,
|
|
|
|
Proto: &pbresource.Tombstone{},
|
|
|
|
})
|
|
|
|
return registry
|
2023-03-14 18:30:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *TypeRegistry) Register(registration Registration) {
|
|
|
|
typ := registration.Type
|
|
|
|
if typ.Group == "" || typ.GroupVersion == "" || typ.Kind == "" {
|
|
|
|
panic("type field(s) cannot be empty")
|
|
|
|
}
|
|
|
|
|
2023-06-26 12:25:14 +00:00
|
|
|
switch {
|
|
|
|
case !groupRegexp.MatchString(typ.Group):
|
|
|
|
panic(fmt.Sprintf("Type.Group must be in snake_case. Got: %q", typ.Group))
|
|
|
|
case !groupVersionRegexp.MatchString(typ.GroupVersion):
|
2023-09-22 16:51:15 +00:00
|
|
|
panic(fmt.Sprintf("Type.GroupVersion must be lowercase, start with `v`, and end with a number (e.g. `v2` or `v2beta1`). Got: %q", typ.Group))
|
2023-06-26 12:25:14 +00:00
|
|
|
case !kindRegexp.MatchString(typ.Kind):
|
|
|
|
panic(fmt.Sprintf("Type.Kind must be in PascalCase. Got: %q", typ.Kind))
|
|
|
|
}
|
|
|
|
|
2023-09-01 14:44:53 +00:00
|
|
|
if registration.Proto == nil {
|
|
|
|
panic("Proto field is required.")
|
|
|
|
}
|
|
|
|
|
|
|
|
if registration.Scope == ScopeUndefined && !isUndefinedScopeAllowed(typ) {
|
|
|
|
panic(fmt.Sprintf("scope required for %s. Got: %q", typ, registration.Scope))
|
|
|
|
}
|
|
|
|
|
2023-06-26 12:25:14 +00:00
|
|
|
r.lock.Lock()
|
|
|
|
defer r.lock.Unlock()
|
|
|
|
|
2023-03-14 18:30:25 +00:00
|
|
|
key := ToGVK(registration.Type)
|
|
|
|
if _, ok := r.registrations[key]; ok {
|
|
|
|
panic(fmt.Sprintf("resource type %s already registered", key))
|
|
|
|
}
|
|
|
|
|
2023-04-11 11:10:14 +00:00
|
|
|
// set default acl hooks for those not provided
|
|
|
|
if registration.ACLs == nil {
|
|
|
|
registration.ACLs = &ACLHooks{}
|
|
|
|
}
|
|
|
|
if registration.ACLs.Read == nil {
|
2023-09-22 14:53:55 +00:00
|
|
|
registration.ACLs.Read = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.ID, _ *pbresource.Resource) error {
|
2023-08-07 21:37:03 +00:00
|
|
|
return authz.ToAllowAuthorizer().OperatorReadAllowed(authzContext)
|
2023-04-11 11:10:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if registration.ACLs.Write == nil {
|
2023-08-10 14:53:38 +00:00
|
|
|
registration.ACLs.Write = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.Resource) error {
|
|
|
|
return authz.ToAllowAuthorizer().OperatorWriteAllowed(authzContext)
|
2023-04-11 11:10:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if registration.ACLs.List == nil {
|
2023-08-15 21:57:59 +00:00
|
|
|
registration.ACLs.List = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext) error {
|
2023-04-11 11:10:14 +00:00
|
|
|
return authz.ToAllowAuthorizer().OperatorReadAllowed(&acl.AuthorizerContext{})
|
|
|
|
}
|
|
|
|
}
|
2023-04-11 11:55:32 +00:00
|
|
|
|
|
|
|
// default validation to a no-op
|
|
|
|
if registration.Validate == nil {
|
|
|
|
registration.Validate = func(resource *pbresource.Resource) error { return nil }
|
|
|
|
}
|
|
|
|
|
2023-04-12 21:50:07 +00:00
|
|
|
// default mutate to a no-op
|
|
|
|
if registration.Mutate == nil {
|
|
|
|
registration.Mutate = func(resource *pbresource.Resource) error { return nil }
|
|
|
|
}
|
|
|
|
|
2023-03-14 18:30:25 +00:00
|
|
|
r.registrations[key] = registration
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *TypeRegistry) Resolve(typ *pbresource.Type) (reg Registration, ok bool) {
|
|
|
|
r.lock.RLock()
|
|
|
|
defer r.lock.RUnlock()
|
|
|
|
|
|
|
|
if registration, ok := r.registrations[ToGVK(typ)]; ok {
|
|
|
|
return registration, true
|
|
|
|
}
|
|
|
|
return Registration{}, false
|
|
|
|
}
|
|
|
|
|
2023-08-04 18:27:48 +00:00
|
|
|
func (r *TypeRegistry) Types() []Registration {
|
|
|
|
r.lock.RLock()
|
|
|
|
defer r.lock.RUnlock()
|
|
|
|
|
|
|
|
types := make([]Registration, 0, len(r.registrations))
|
|
|
|
for _, v := range r.registrations {
|
|
|
|
types = append(types, v)
|
|
|
|
}
|
|
|
|
return types
|
|
|
|
}
|
|
|
|
|
2023-03-14 18:30:25 +00:00
|
|
|
func ToGVK(resourceType *pbresource.Type) string {
|
2023-04-11 11:10:14 +00:00
|
|
|
return fmt.Sprintf("%s.%s.%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind)
|
2023-03-14 18:30:25 +00:00
|
|
|
}
|
2023-08-11 19:52:51 +00:00
|
|
|
|
|
|
|
func ParseGVK(gvk string) (*pbresource.Type, error) {
|
|
|
|
parts := strings.Split(gvk, ".")
|
|
|
|
if len(parts) != 3 {
|
|
|
|
return nil, fmt.Errorf("GVK string must be in the form <Group>.<GroupVersion>.<Kind>, got: %s", gvk)
|
|
|
|
}
|
|
|
|
return &pbresource.Type{
|
|
|
|
Group: parts[0],
|
|
|
|
GroupVersion: parts[1],
|
|
|
|
Kind: parts[2],
|
|
|
|
}, nil
|
|
|
|
}
|