consul/internal/resource/registry.go

203 lines
6.6 KiB
Go
Raw Permalink Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-14 18:30:25 +00:00
package resource
import (
"errors"
2023-03-14 18:30:25 +00:00
"fmt"
"regexp"
"strings"
2023-03-14 18:30:25 +00:00
"sync"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/storage"
2023-03-14 18:30:25 +00:00
"github.com/hashicorp/consul/proto-public/pbresource"
)
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]+$`)
// 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,
}
)
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)
Types() []Registration
2023-03-14 18:30:25 +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
var ErrNeedResource = errors.New("authorization check requires the entire resource")
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
type ACLHooks struct {
// Read is used to authorize Read RPCs and to filter results in List
// RPCs.
//
// It can be called an ID and possibly a Resource. The check will first
// attempt to use the ID and if the hook returns ErrNeedResource, then the
// check will be deferred until the data is fetched from the storage layer.
//
// If it is omitted, `operator:read` permission is assumed.
Read ACLAuthorizeReadHook
// Write is used to authorize Write and Delete RPCs.
//
// If it is omitted, `operator:write` permission is assumed.
Write ACLAuthorizeWriteHook
// List is used to authorize List RPCs.
//
// If it is omitted, we only filter the results using Read.
List ACLAuthorizeListHook
}
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 {
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")
}
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):
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))
case !kindRegexp.MatchString(typ.Kind):
panic(fmt.Sprintf("Type.Kind must be in PascalCase. Got: %q", typ.Kind))
}
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))
}
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))
}
// set default acl hooks for those not provided
if registration.ACLs == nil {
registration.ACLs = &ACLHooks{}
}
if registration.ACLs.Read == nil {
registration.ACLs.Read = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.ID, _ *pbresource.Resource) error {
return authz.ToAllowAuthorizer().OperatorReadAllowed(authzContext)
}
}
if registration.ACLs.Write == nil {
registration.ACLs.Write = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.Resource) error {
return authz.ToAllowAuthorizer().OperatorWriteAllowed(authzContext)
}
}
if registration.ACLs.List == nil {
registration.ACLs.List = func(authz acl.Authorizer, authzContext *acl.AuthorizerContext) error {
return authz.ToAllowAuthorizer().OperatorReadAllowed(&acl.AuthorizerContext{})
}
}
// default validation to a no-op
if registration.Validate == nil {
registration.Validate = func(resource *pbresource.Resource) error { return nil }
}
// 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
}
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 {
return fmt.Sprintf("%s.%s.%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind)
2023-03-14 18:30:25 +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
}