open-nomad/nomad/structs/acl.go
James Rasell 8295d0e516
acl: add validation to binding rule selector on upsert. (#16210)
* acl: add validation to binding rule selector on upsert.

* docs: add more information on binding rule selector escaping.
2023-02-17 15:38:55 +01:00

1442 lines
42 KiB
Go

package structs
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"time"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-set"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/lib/lang"
"golang.org/x/crypto/blake2b"
"golang.org/x/exp/slices"
"oss.indeed.com/go/libtime"
)
const (
// ACLUpsertPoliciesRPCMethod is the RPC method for batch creating or
// modifying ACL policies.
//
// Args: ACLPolicyUpsertRequest
// Reply: GenericResponse
ACLUpsertPoliciesRPCMethod = "ACL.UpsertPolicies"
// ACLUpsertTokensRPCMethod is the RPC method for batch creating or
// modifying ACL tokens.
//
// Args: ACLTokenUpsertRequest
// Reply: ACLTokenUpsertResponse
ACLUpsertTokensRPCMethod = "ACL.UpsertTokens"
// ACLDeleteTokensRPCMethod is the RPC method for batch deleting ACL
// tokens.
//
// Args: ACLTokenDeleteRequest
// Reply: GenericResponse
ACLDeleteTokensRPCMethod = "ACL.DeleteTokens"
// ACLUpsertRolesRPCMethod is the RPC method for batch creating or
// modifying ACL roles.
//
// Args: ACLRolesUpsertRequest
// Reply: ACLRolesUpsertResponse
ACLUpsertRolesRPCMethod = "ACL.UpsertRoles"
// ACLDeleteRolesByIDRPCMethod the RPC method for batch deleting ACL
// roles by their ID.
//
// Args: ACLRolesDeleteByIDRequest
// Reply: ACLRolesDeleteByIDResponse
ACLDeleteRolesByIDRPCMethod = "ACL.DeleteRolesByID"
// ACLListRolesRPCMethod is the RPC method for listing ACL roles.
//
// Args: ACLRolesListRequest
// Reply: ACLRolesListResponse
ACLListRolesRPCMethod = "ACL.ListRoles"
// ACLGetRolesByIDRPCMethod is the RPC method for detailing a number of ACL
// roles using their ID. This is an internal only RPC endpoint and used by
// the ACL Role replication process.
//
// Args: ACLRolesByIDRequest
// Reply: ACLRolesByIDResponse
ACLGetRolesByIDRPCMethod = "ACL.GetRolesByID"
// ACLGetRoleByIDRPCMethod is the RPC method for detailing an individual
// ACL role using its ID.
//
// Args: ACLRoleByIDRequest
// Reply: ACLRoleByIDResponse
ACLGetRoleByIDRPCMethod = "ACL.GetRoleByID"
// ACLGetRoleByNameRPCMethod is the RPC method for detailing an individual
// ACL role using its name.
//
// Args: ACLRoleByNameRequest
// Reply: ACLRoleByNameResponse
ACLGetRoleByNameRPCMethod = "ACL.GetRoleByName"
// ACLUpsertAuthMethodsRPCMethod is the RPC method for batch creating or
// modifying auth methods.
//
// Args: ACLAuthMethodsUpsertRequest
// Reply: ACLAuthMethodUpsertResponse
ACLUpsertAuthMethodsRPCMethod = "ACL.UpsertAuthMethods"
// ACLDeleteAuthMethodsRPCMethod is the RPC method for batch deleting auth
// methods.
//
// Args: ACLAuthMethodDeleteRequest
// Reply: ACLAuthMethodDeleteResponse
ACLDeleteAuthMethodsRPCMethod = "ACL.DeleteAuthMethods"
// ACLListAuthMethodsRPCMethod is the RPC method for listing auth methods.
//
// Args: ACLAuthMethodListRequest
// Reply: ACLAuthMethodListResponse
ACLListAuthMethodsRPCMethod = "ACL.ListAuthMethods"
// ACLGetAuthMethodRPCMethod is the RPC method for detailing an individual
// auth method using its name.
//
// Args: ACLAuthMethodGetRequest
// Reply: ACLAuthMethodGetResponse
ACLGetAuthMethodRPCMethod = "ACL.GetAuthMethod"
// ACLGetAuthMethodsRPCMethod is the RPC method for getting multiple auth
// methods using their names.
//
// Args: ACLAuthMethodsGetRequest
// Reply: ACLAuthMethodsGetResponse
ACLGetAuthMethodsRPCMethod = "ACL.GetAuthMethods"
// ACLUpsertBindingRulesRPCMethod is the RPC method for batch creating or
// modifying binding rules.
//
// Args: ACLBindingRulesUpsertRequest
// Reply: ACLBindingRulesUpsertResponse
ACLUpsertBindingRulesRPCMethod = "ACL.UpsertBindingRules"
// ACLDeleteBindingRulesRPCMethod is the RPC method for batch deleting
// binding rules.
//
// Args: ACLBindingRulesDeleteRequest
// Reply: ACLBindingRulesDeleteResponse
ACLDeleteBindingRulesRPCMethod = "ACL.DeleteBindingRules"
// ACLListBindingRulesRPCMethod is the RPC method listing binding rules.
//
// Args: ACLBindingRulesListRequest
// Reply: ACLBindingRulesListResponse
ACLListBindingRulesRPCMethod = "ACL.ListBindingRules"
// ACLGetBindingRulesRPCMethod is the RPC method for getting multiple
// binding rules using their IDs.
//
// Args: ACLBindingRulesRequest
// Reply: ACLBindingRulesResponse
ACLGetBindingRulesRPCMethod = "ACL.GetBindingRules"
// ACLGetBindingRuleRPCMethod is the RPC method for detailing an individual
// binding rule using its ID.
//
// Args: ACLBindingRuleRequest
// Reply: ACLBindingRuleResponse
ACLGetBindingRuleRPCMethod = "ACL.GetBindingRule"
// ACLOIDCAuthURLRPCMethod is the RPC method for starting the OIDC login
// workflow. It generates the OIDC provider URL which will be used for user
// authentication.
//
// Args: ACLOIDCAuthURLRequest
// Reply: ACLOIDCAuthURLResponse
ACLOIDCAuthURLRPCMethod = "ACL.OIDCAuthURL"
// ACLOIDCCompleteAuthRPCMethod is the RPC method for completing the OIDC
// login workflow. It exchanges the OIDC provider token for a Nomad ACL
// token with roles as defined within the remote provider.
//
// Args: ACLOIDCCompleteAuthRequest
// Reply: ACLOIDCCompleteAuthResponse
ACLOIDCCompleteAuthRPCMethod = "ACL.OIDCCompleteAuth"
)
const (
// ACLMaxExpiredBatchSize is the maximum number of expired ACL tokens that
// will be garbage collected in a single trigger. This number helps limit
// the replication pressure due to expired token deletion. If there are a
// large number of expired tokens pending garbage collection, this value is
// a potential limiting factor.
ACLMaxExpiredBatchSize = 4096
// maxACLRoleDescriptionLength limits an ACL roles description length.
maxACLRoleDescriptionLength = 256
// maxACLBindingRuleDescriptionLength limits an ACL binding rules
// description length and should be used to validate the object.
maxACLBindingRuleDescriptionLength = 256
)
var (
// ValidACLRoleName is used to validate an ACL role name.
ValidACLRoleName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
// ValidACLAuthMethod is used to validate an ACL auth method name.
ValidACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
)
type ACLCacheEntry[T any] lang.Pair[T, time.Time]
func (e ACLCacheEntry[T]) Age() time.Duration {
return time.Since(e.Second)
}
func (e ACLCacheEntry[T]) Get() T {
return e.First
}
// An ACLCache caches ACL tokens by their policy content.
type ACLCache[T any] struct {
*lru.TwoQueueCache[string, ACLCacheEntry[T]]
clock libtime.Clock
}
func (c *ACLCache[T]) Add(key string, item T) {
c.AddAtTime(key, item, c.clock.Now())
}
func (c *ACLCache[T]) AddAtTime(key string, item T, now time.Time) {
c.TwoQueueCache.Add(key, ACLCacheEntry[T]{
First: item,
Second: now,
})
}
func NewACLCache[T any](size int) *ACLCache[T] {
c, err := lru.New2Q[string, ACLCacheEntry[T]](size)
if err != nil {
panic(err) // not possible
}
return &ACLCache[T]{
TwoQueueCache: c,
clock: libtime.SystemClock(),
}
}
// ACLTokenRoleLink is used to link an ACL token to an ACL role. The ACL token
// can therefore inherit all the ACL policy permissions that the ACL role
// contains.
type ACLTokenRoleLink struct {
// ID is the ACLRole.ID UUID. This field is immutable and represents the
// absolute truth for the link.
ID string
// Name is the human friendly identifier for the ACL role and is a
// convenience field for operators. This field is always resolved to the
// ID and discarded before the token is stored in state. This is because
// operators can change the name of an ACL role.
Name string
}
// Canonicalize performs basic canonicalization on the ACL token object. It is
// important for callers to understand certain fields such as AccessorID are
// set if it is empty, so copies should be taken if needed before calling this
// function.
func (a *ACLToken) Canonicalize() {
// If the accessor ID is empty, it means this is creation of a new token,
// therefore we need to generate base information.
if a.AccessorID == "" {
a.AccessorID = uuid.Generate()
a.SecretID = uuid.Generate()
a.CreateTime = time.Now().UTC()
// If the user has not set the expiration time, but has provided a TTL, we
// calculate and populate the former filed.
if a.ExpirationTime == nil && a.ExpirationTTL != 0 {
a.ExpirationTime = pointer.Of(a.CreateTime.Add(a.ExpirationTTL))
}
}
}
// Validate is used to check a token for reasonableness
func (a *ACLToken) Validate(minTTL, maxTTL time.Duration, existing *ACLToken) error {
var mErr multierror.Error
// The human friendly name of an ACL token cannot exceed 256 characters.
if len(a.Name) > maxTokenNameLength {
mErr.Errors = append(mErr.Errors, errors.New("token name too long"))
}
// The type of an ACL token must be set. An ACL token of type client must
// have associated policies or roles, whereas a management token cannot be
// associated with policies.
switch a.Type {
case ACLClientToken:
if len(a.Policies) == 0 && len(a.Roles) == 0 {
mErr.Errors = append(mErr.Errors, errors.New("client token missing policies or roles"))
}
case ACLManagementToken:
if len(a.Policies) != 0 || len(a.Roles) != 0 {
mErr.Errors = append(mErr.Errors, errors.New("management token cannot be associated with policies or roles"))
}
default:
mErr.Errors = append(mErr.Errors, errors.New("token type must be client or management"))
}
// There are different validation rules depending on whether the ACL token
// is being created or updated.
switch existing {
case nil:
if a.ExpirationTTL < 0 {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("token expiration TTL '%s' should not be negative", a.ExpirationTTL))
}
if a.ExpirationTime != nil && !a.ExpirationTime.IsZero() {
if a.CreateTime.After(*a.ExpirationTime) {
mErr.Errors = append(mErr.Errors, errors.New("expiration time cannot be before create time"))
}
// Create a time duration which details the time-til-expiry, so we can
// check this against the regions max and min values.
expiresIn := a.ExpirationTime.Sub(a.CreateTime)
if expiresIn > maxTTL {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("expiration time cannot be more than %s in the future (was %s)",
maxTTL, expiresIn))
} else if expiresIn < minTTL {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("expiration time cannot be less than %s in the future (was %s)",
minTTL, expiresIn))
}
}
default:
if existing.Global != a.Global {
mErr.Errors = append(mErr.Errors, errors.New("cannot toggle global mode"))
}
if existing.ExpirationTTL != a.ExpirationTTL {
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
}
if existing.ExpirationTime != a.ExpirationTime {
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
}
}
return mErr.ErrorOrNil()
}
// HasExpirationTime checks whether the ACL token has an expiration time value
// set.
func (a *ACLToken) HasExpirationTime() bool {
if a == nil || a.ExpirationTime == nil {
return false
}
return !a.ExpirationTime.IsZero()
}
// IsExpired compares the ACLToken.ExpirationTime against the passed t to
// identify whether the token is considered expired. The function can be called
// without checking whether the ACL token has an expiry time.
func (a *ACLToken) IsExpired(t time.Time) bool {
// Check the token has an expiration time before potentially modifying the
// supplied time. This allows us to avoid extra work, if it isn't needed.
if !a.HasExpirationTime() {
return false
}
// Check and ensure the time location is set to UTC. This is vital for
// consistency with multi-region global tokens.
if t.Location() != time.UTC {
t = t.UTC()
}
return a.ExpirationTime.Before(t) || t.IsZero()
}
// HasRoles checks if a given set of role IDs are assigned to the ACL token. It
// does not account for management tokens, therefore it is the responsibility
// of the caller to perform this check, if required.
func (a *ACLToken) HasRoles(roleIDs []string) bool {
// Generate a set of role IDs that the token is assigned.
roleSet := set.FromFunc(a.Roles, func(roleLink *ACLTokenRoleLink) string { return roleLink.ID })
// Iterate the role IDs within the request and check whether these are
// present within the token assignment.
for _, roleID := range roleIDs {
if !roleSet.Contains(roleID) {
return false
}
}
return true
}
// MarshalJSON implements the json.Marshaler interface and allows
// ACLToken.ExpirationTTL to be marshaled correctly.
func (a *ACLToken) MarshalJSON() ([]byte, error) {
type Alias ACLToken
exported := &struct {
ExpirationTTL string
*Alias
}{
ExpirationTTL: a.ExpirationTTL.String(),
Alias: (*Alias)(a),
}
if a.ExpirationTTL == 0 {
exported.ExpirationTTL = ""
}
return json.Marshal(exported)
}
// UnmarshalJSON implements the json.Unmarshaler interface and allows
// ACLToken.ExpirationTTL to be unmarshalled correctly.
func (a *ACLToken) UnmarshalJSON(data []byte) (err error) {
type Alias ACLToken
aux := &struct {
ExpirationTTL interface{}
Hash string
*Alias
}{
Alias: (*Alias)(a),
}
if err = json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ExpirationTTL != nil {
switch v := aux.ExpirationTTL.(type) {
case string:
if v != "" {
if a.ExpirationTTL, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
a.ExpirationTTL = time.Duration(v)
}
}
if aux.Hash != "" {
a.Hash = []byte(aux.Hash)
}
return nil
}
// ACLRole is an abstraction for the ACL system which allows the grouping of
// ACL policies into a single object. ACL tokens can be created and linked to
// a role; the token then inherits all the permissions granted by the policies.
type ACLRole struct {
// ID is an internally generated UUID for this role and is controlled by
// Nomad.
ID string
// Name is unique across the entire set of federated clusters and is
// supplied by the operator on role creation. The name can be modified by
// updating the role and including the Nomad generated ID. This update will
// not affect tokens created and linked to this role. This is a required
// field.
Name string
// Description is a human-readable, operator set description that can
// provide additional context about the role. This is an operational field.
Description string
// Policies is an array of ACL policy links. Although currently policies
// can only be linked using their name, in the future we will want to add
// IDs also and thus allow operators to specify either a name, an ID, or
// both.
Policies []*ACLRolePolicyLink
// Hash is the hashed value of the role and is generated using all fields
// above this point.
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
// rather than a list of strings as in the future we will want to add IDs to
// policies and then link via these.
type ACLRolePolicyLink struct {
// Name is the ACLPolicy.Name value which will be linked to the ACL role.
Name string
}
// SetHash is used to compute and set the hash of the ACL role. This should be
// called every and each time a user specified field on the role is changed
// before updating the Nomad state store.
func (a *ACLRole) SetHash() []byte {
// Initialize a 256bit Blake2 hash (32 bytes).
hash, err := blake2b.New256(nil)
if err != nil {
panic(err)
}
// Write all the user set fields.
_, _ = hash.Write([]byte(a.Name))
_, _ = hash.Write([]byte(a.Description))
for _, policyLink := range a.Policies {
_, _ = hash.Write([]byte(policyLink.Name))
}
// Finalize the hash.
hashVal := hash.Sum(nil)
// Set and return the hash.
a.Hash = hashVal
return hashVal
}
// Validate ensure the ACL role contains valid information which meets Nomad's
// internal requirements. This does not include any state calls, such as
// ensuring the linked policies exist.
func (a *ACLRole) Validate() error {
var mErr multierror.Error
if !ValidACLRoleName.MatchString(a.Name) {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
}
if len(a.Description) > maxACLRoleDescriptionLength {
mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
}
if len(a.Policies) < 1 {
mErr.Errors = append(mErr.Errors, errors.New("at least one policy should be specified"))
}
return mErr.ErrorOrNil()
}
// Canonicalize performs basic canonicalization on the ACL role object. It is
// important for callers to understand certain fields such as ID are set if it
// is empty, so copies should be taken if needed before calling this function.
func (a *ACLRole) Canonicalize() {
if a.ID == "" {
a.ID = uuid.Generate()
}
}
// Equal performs an equality check on the two service registrations. It
// handles nil objects.
func (a *ACLRole) Equal(o *ACLRole) bool {
if a == nil || o == nil {
return a == o
}
if len(a.Hash) == 0 {
a.SetHash()
}
if len(o.Hash) == 0 {
o.SetHash()
}
return bytes.Equal(a.Hash, o.Hash)
}
// Copy creates a deep copy of the ACL role. This copy can then be safely
// modified. It handles nil objects.
func (a *ACLRole) Copy() *ACLRole {
if a == nil {
return nil
}
c := new(ACLRole)
*c = *a
c.Policies = slices.Clone(a.Policies)
c.Hash = slices.Clone(a.Hash)
return c
}
// Stub converts the ACLRole object into a ACLRoleListStub object.
func (a *ACLRole) Stub() *ACLRoleListStub {
return &ACLRoleListStub{
ID: a.ID,
Name: a.Name,
Description: a.Description,
Policies: a.Policies,
Hash: a.Hash,
CreateIndex: a.CreateIndex,
ModifyIndex: a.ModifyIndex,
}
}
// ACLRoleListStub is the stub object returned when performing a listing of ACL
// roles. While it might not currently be different to the full response
// object, it allows us to future-proof the RPC in the event the ACLRole object
// grows over time.
type ACLRoleListStub struct {
// ID is an internally generated UUID for this role and is controlled by
// Nomad.
ID string
// Name is unique across the entire set of federated clusters and is
// supplied by the operator on role creation. The name can be modified by
// updating the role and including the Nomad generated ID. This update will
// not affect tokens created and linked to this role. This is a required
// field.
Name string
// Description is a human-readable, operator set description that can
// provide additional context about the role. This is an operational field.
Description string
// Policies is an array of ACL policy links. Although currently policies
// can only be linked using their name, in the future we will want to add
// IDs also and thus allow operators to specify either a name, an ID, or
// both.
Policies []*ACLRolePolicyLink
// Hash is the hashed value of the role and is generated using all fields
// above this point.
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
// ACLRolesUpsertRequest is the request object used to upsert one or more ACL
// roles.
type ACLRolesUpsertRequest struct {
ACLRoles []*ACLRole
// AllowMissingPolicies skips the ACL Role policy link verification and is
// used by the replication process. The replication cannot ensure policies
// are present before ACL Roles are replicated.
AllowMissingPolicies bool
WriteRequest
}
// ACLRolesUpsertResponse is the response object when one or more ACL roles
// have been successfully upserted into state.
type ACLRolesUpsertResponse struct {
ACLRoles []*ACLRole
WriteMeta
}
// ACLRolesDeleteByIDRequest is the request object to delete one or more ACL
// roles using the role ID.
type ACLRolesDeleteByIDRequest struct {
ACLRoleIDs []string
WriteRequest
}
// ACLRolesDeleteByIDResponse is the response object when performing a deletion
// of one or more ACL roles using the role ID.
type ACLRolesDeleteByIDResponse struct {
WriteMeta
}
// ACLRolesListRequest is the request object when performing ACL role listings.
type ACLRolesListRequest struct {
QueryOptions
}
// ACLRolesListResponse is the response object when performing ACL role
// listings.
type ACLRolesListResponse struct {
ACLRoles []*ACLRoleListStub
QueryMeta
}
// ACLRolesByIDRequest is the request object when performing a lookup of
// multiple roles by the ID.
type ACLRolesByIDRequest struct {
ACLRoleIDs []string
QueryOptions
}
// ACLRolesByIDResponse is the response object when performing a lookup of
// multiple roles by their IDs.
type ACLRolesByIDResponse struct {
ACLRoles map[string]*ACLRole
QueryMeta
}
// ACLRoleByIDRequest is the request object to perform a lookup of an ACL
// role using a specific ID.
type ACLRoleByIDRequest struct {
RoleID string
QueryOptions
}
// ACLRoleByIDResponse is the response object when performing a lookup of an
// ACL role matching a specific ID.
type ACLRoleByIDResponse struct {
ACLRole *ACLRole
QueryMeta
}
// ACLRoleByNameRequest is the request object to perform a lookup of an ACL
// role using a specific name.
type ACLRoleByNameRequest struct {
RoleName string
QueryOptions
}
// ACLRoleByNameResponse is the response object when performing a lookup of an
// ACL role matching a specific name.
type ACLRoleByNameResponse struct {
ACLRole *ACLRole
QueryMeta
}
// ACLAuthMethod is used to capture the properties of an authentication method
// used for single sing-on
type ACLAuthMethod struct {
Name string
Type string
TokenLocality string // is the token valid locally or globally?
MaxTokenTTL time.Duration
Default bool
Config *ACLAuthMethodConfig
Hash []byte
CreateTime time.Time
ModifyTime time.Time
CreateIndex uint64
ModifyIndex uint64
}
// SetHash is used to compute and set the hash of the ACL auth method. This
// should be called every and each time a user specified field on the method is
// changed before updating the Nomad state store.
func (a *ACLAuthMethod) SetHash() []byte {
// Initialize a 256bit Blake2 hash (32 bytes).
hash, err := blake2b.New256(nil)
if err != nil {
panic(err)
}
_, _ = hash.Write([]byte(a.Name))
_, _ = hash.Write([]byte(a.Type))
_, _ = hash.Write([]byte(a.TokenLocality))
_, _ = hash.Write([]byte(a.MaxTokenTTL.String()))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Default)))
if a.Config != nil {
_, _ = hash.Write([]byte(a.Config.OIDCDiscoveryURL))
_, _ = hash.Write([]byte(a.Config.OIDCClientID))
_, _ = hash.Write([]byte(a.Config.OIDCClientSecret))
for _, ba := range a.Config.BoundAudiences {
_, _ = hash.Write([]byte(ba))
}
for _, uri := range a.Config.AllowedRedirectURIs {
_, _ = hash.Write([]byte(uri))
}
for _, pem := range a.Config.DiscoveryCaPem {
_, _ = hash.Write([]byte(pem))
}
for _, sa := range a.Config.SigningAlgs {
_, _ = hash.Write([]byte(sa))
}
for k, v := range a.Config.ClaimMappings {
_, _ = hash.Write([]byte(k))
_, _ = hash.Write([]byte(v))
}
for k, v := range a.Config.ListClaimMappings {
_, _ = hash.Write([]byte(k))
_, _ = hash.Write([]byte(v))
}
}
// Finalize the hash.
hashVal := hash.Sum(nil)
// Set and return the hash.
a.Hash = hashVal
return hashVal
}
// MarshalJSON implements the json.Marshaler interface and allows
// ACLAuthMethod.MaxTokenTTL to be marshaled correctly.
func (a *ACLAuthMethod) MarshalJSON() ([]byte, error) {
type Alias ACLAuthMethod
exported := &struct {
MaxTokenTTL string
*Alias
}{
MaxTokenTTL: a.MaxTokenTTL.String(),
Alias: (*Alias)(a),
}
if a.MaxTokenTTL == 0 {
exported.MaxTokenTTL = ""
}
return json.Marshal(exported)
}
// UnmarshalJSON implements the json.Unmarshaler interface and allows
// ACLAuthMethod.MaxTokenTTL to be unmarshalled correctly.
func (a *ACLAuthMethod) UnmarshalJSON(data []byte) (err error) {
type Alias ACLAuthMethod
aux := &struct {
MaxTokenTTL interface{}
*Alias
}{
Alias: (*Alias)(a),
}
if err = json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.MaxTokenTTL != nil {
switch v := aux.MaxTokenTTL.(type) {
case string:
if a.MaxTokenTTL, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
a.MaxTokenTTL = time.Duration(v)
}
}
return nil
}
func (a *ACLAuthMethod) Stub() *ACLAuthMethodStub {
return &ACLAuthMethodStub{
Name: a.Name,
Type: a.Type,
Default: a.Default,
Hash: a.Hash,
CreateIndex: a.CreateIndex,
ModifyIndex: a.ModifyIndex,
}
}
func (a *ACLAuthMethod) Equal(other *ACLAuthMethod) bool {
if a == nil || other == nil {
return a == other
}
if len(a.Hash) == 0 {
a.SetHash()
}
if len(other.Hash) == 0 {
other.SetHash()
}
return bytes.Equal(a.Hash, other.Hash)
}
// Copy creates a deep copy of the ACL auth method. This copy can then be safely
// modified. It handles nil objects.
func (a *ACLAuthMethod) Copy() *ACLAuthMethod {
if a == nil {
return nil
}
c := new(ACLAuthMethod)
*c = *a
c.Hash = slices.Clone(a.Hash)
c.Config = a.Config.Copy()
return c
}
// Canonicalize performs basic canonicalization on the ACL auth method object.
func (a *ACLAuthMethod) Canonicalize() {
t := time.Now().UTC()
if a.CreateTime.IsZero() {
a.CreateTime = t
}
a.ModifyTime = t
}
// Merge merges auth method a with method b. It sets all required empty fields
// of method a to corresponding values of method b, except for "default" and
// "name."
func (a *ACLAuthMethod) Merge(b *ACLAuthMethod) {
if b != nil {
a.Type = helper.Merge(a.Type, b.Type)
a.TokenLocality = helper.Merge(a.TokenLocality, b.TokenLocality)
a.MaxTokenTTL = helper.Merge(a.MaxTokenTTL, b.MaxTokenTTL)
a.Config = helper.Merge(a.Config, b.Config)
}
}
// Validate returns an error is the ACLAuthMethod is invalid.
//
// TODO revisit possible other validity conditions in the future
func (a *ACLAuthMethod) Validate(minTTL, maxTTL time.Duration) error {
var mErr multierror.Error
if !ValidACLAuthMethod.MatchString(a.Name) {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
}
if !slices.Contains([]string{"local", "global"}, a.TokenLocality) {
mErr.Errors = append(
mErr.Errors, fmt.Errorf("invalid token locality '%s'", a.TokenLocality))
}
if a.Type != "OIDC" {
mErr.Errors = append(
mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
}
if minTTL > a.MaxTokenTTL || a.MaxTokenTTL > maxTTL {
mErr.Errors = append(mErr.Errors, fmt.Errorf(
"invalid MaxTokenTTL value '%s' (should be between %s and %s)",
a.MaxTokenTTL.String(), minTTL.String(), maxTTL.String()))
}
return mErr.ErrorOrNil()
}
// TokenLocalityIsGlobal returns whether the auth method creates global ACL
// tokens or not.
func (a *ACLAuthMethod) TokenLocalityIsGlobal() bool { return a.TokenLocality == "global" }
// ACLAuthMethodConfig is used to store configuration of an auth method
type ACLAuthMethodConfig struct {
OIDCDiscoveryURL string
OIDCClientID string
OIDCClientSecret string
OIDCScopes []string
BoundAudiences []string
AllowedRedirectURIs []string
DiscoveryCaPem []string
SigningAlgs []string
ClaimMappings map[string]string
ListClaimMappings map[string]string
}
func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
if a == nil {
return nil
}
c := new(ACLAuthMethodConfig)
*c = *a
c.OIDCScopes = slices.Clone(a.OIDCScopes)
c.BoundAudiences = slices.Clone(a.BoundAudiences)
c.AllowedRedirectURIs = slices.Clone(a.AllowedRedirectURIs)
c.DiscoveryCaPem = slices.Clone(a.DiscoveryCaPem)
c.SigningAlgs = slices.Clone(a.SigningAlgs)
return c
}
// ACLAuthClaims is the claim mapping of the OIDC auth method in a format that
// can be used with go-bexpr. This structure is used during rule binding
// evaluation.
type ACLAuthClaims struct {
Value map[string]string `bexpr:"value"`
List map[string][]string `bexpr:"list"`
}
// ACLAuthMethodStub is used for listing ACL auth methods
type ACLAuthMethodStub struct {
Name string
Type string
Default bool
// Hash is the hashed value of the auth-method and is generated using all
// fields from the full object except the create and modify times and
// indexes.
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
// ACLAuthMethodListRequest is used to list auth methods
type ACLAuthMethodListRequest struct {
QueryOptions
}
// ACLAuthMethodListResponse is used to list auth methods
type ACLAuthMethodListResponse struct {
AuthMethods []*ACLAuthMethodStub
QueryMeta
}
// ACLAuthMethodGetRequest is used to query a specific auth method
type ACLAuthMethodGetRequest struct {
MethodName string
QueryOptions
}
// ACLAuthMethodGetResponse is used to return a single auth method
type ACLAuthMethodGetResponse struct {
AuthMethod *ACLAuthMethod
QueryMeta
}
// ACLAuthMethodsGetRequest is used to query a set of auth methods
type ACLAuthMethodsGetRequest struct {
Names []string
QueryOptions
}
// ACLAuthMethodsGetResponse is used to return a set of auth methods
type ACLAuthMethodsGetResponse struct {
AuthMethods map[string]*ACLAuthMethod
QueryMeta
}
// ACLAuthMethodUpsertRequest is used to upsert a set of auth methods
type ACLAuthMethodUpsertRequest struct {
AuthMethods []*ACLAuthMethod
WriteRequest
}
// ACLAuthMethodUpsertResponse is a response of the upsert ACL auth methods
// operation
type ACLAuthMethodUpsertResponse struct {
AuthMethods []*ACLAuthMethod
WriteMeta
}
// ACLAuthMethodDeleteRequest is used to delete a set of auth methods by their
// name
type ACLAuthMethodDeleteRequest struct {
Names []string
WriteRequest
}
// ACLAuthMethodDeleteResponse is a response of the delete ACL auth methods
// operation
type ACLAuthMethodDeleteResponse struct {
WriteMeta
}
type ACLWhoAmIResponse struct {
Identity *AuthenticatedIdentity
QueryMeta
}
// ACLBindingRule contains a direct relation to an ACLAuthMethod and represents
// a rule to apply when logging in via the named AuthMethod. This allows the
// transformation of OIDC provider claims, to Nomad based ACL concepts such as
// ACL Roles and Policies.
type ACLBindingRule struct {
// ID is an internally generated UUID for this rule and is controlled by
// Nomad.
ID string
// Description is a human-readable, operator set description that can
// provide additional context about the binding role. This is an
// operational field.
Description string
// AuthMethod is the name of the auth method for which this rule applies
// to. This is required and the method must exist within state before the
// cluster administrator can create the rule.
AuthMethod string
// Selector is an expression that matches against verified identity
// attributes returned from the auth method during login. This is optional
// and when not set, provides a catch-all rule.
Selector string
// BindType adjusts how this binding rule is applied at login time. The
// valid values are ACLBindingRuleBindTypeRole,
// ACLBindingRuleBindTypePolicy, and ACLBindingRuleBindTypeManagement.
BindType string
// BindName is the target of the binding. Can be lightly templated using
// HIL ${foo} syntax from available field names. How it is used depends
// upon the BindType.
BindName string
// Hash is the hashed value of the binding rule and is generated using all
// fields from the full object except the create and modify times and
// indexes.
Hash []byte
CreateTime time.Time
ModifyTime time.Time
CreateIndex uint64
ModifyIndex uint64
}
const (
// ACLBindingRuleBindTypeRole is the ACL binding rule bind type that only
// allows the binding rule to function if a role exists at login-time. The
// role will be specified within the ACLBindingRule.BindName parameter, and
// will identify whether this is an ID or Name.
ACLBindingRuleBindTypeRole = "role"
// ACLBindingRuleBindTypePolicy is the ACL binding rule bind type that
// assigns a policy to the generate ACL token. The role will be specified
// within the ACLBindingRule.BindName parameter, and will be the policy
// name.
ACLBindingRuleBindTypePolicy = "policy"
// ACLBindingRuleBindTypeManagement is the ACL binding rule bind type that
// will generate management ACL tokens when matched.
ACLBindingRuleBindTypeManagement = "management"
)
// Canonicalize performs basic canonicalization on the ACL token object. It is
// important for callers to understand certain fields such as ID are set if it
// is empty, so copies should be taken if needed before calling this function.
func (a *ACLBindingRule) Canonicalize() {
now := time.Now().UTC()
// If the ID is empty, it means this is creation of a new binding rule,
// therefore we need to generate base information.
if a.ID == "" {
a.ID = uuid.Generate()
a.CreateTime = now
}
// The fact this function is being called indicates we are attempting an
// upsert into state. Therefore, update the modify time.
a.ModifyTime = now
}
// Validate ensures the ACL binding rule contains valid information which meets
// Nomad's internal requirements.
func (a *ACLBindingRule) Validate() error {
var mErr multierror.Error
if a.AuthMethod == "" {
mErr.Errors = append(mErr.Errors, errors.New("auth method is missing"))
}
if len(a.Description) > maxACLBindingRuleDescriptionLength {
mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
}
// Depending on the bind type, we have some specific validation. Catching
// the empty string also provides easier to understand feedback to the
// user.
switch a.BindType {
case "":
mErr.Errors = append(mErr.Errors, errors.New("bind type is missing"))
case ACLBindingRuleBindTypeRole, ACLBindingRuleBindTypePolicy:
if a.BindName == "" {
mErr.Errors = append(mErr.Errors, errors.New("bind name is missing"))
}
case ACLBindingRuleBindTypeManagement:
if a.BindName != "" {
mErr.Errors = append(mErr.Errors, errors.New("bind name should be empty"))
}
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("unsupported bind type: %q", a.BindType))
}
// If there is a selector configured, ensure that go-bexpr can parse this.
// Otherwise, the user will get an ambiguous failure when attempting to
// login.
if a.Selector != "" {
if _, err := bexpr.CreateEvaluator(a.Selector, nil); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("selector is invalid: %v", err))
}
}
return mErr.ErrorOrNil()
}
// Merge merges binding rule a with b. It sets all required empty fields of rule
// a to corresponding values of rule b, except for "ID" which must be provided.
func (a *ACLBindingRule) Merge(b *ACLBindingRule) {
a.BindName = helper.Merge(a.BindName, b.BindName)
a.BindType = helper.Merge(a.BindType, b.BindType)
a.AuthMethod = helper.Merge(a.AuthMethod, b.AuthMethod)
}
// SetHash is used to compute and set the hash of the ACL binding rule. This
// should be called every and each time a user specified field on the method is
// changed before updating the Nomad state store.
func (a *ACLBindingRule) SetHash() []byte {
// Initialize a 256bit Blake2 hash (32 bytes).
hash, err := blake2b.New256(nil)
if err != nil {
panic(err)
}
_, _ = hash.Write([]byte(a.ID))
_, _ = hash.Write([]byte(a.Description))
_, _ = hash.Write([]byte(a.AuthMethod))
_, _ = hash.Write([]byte(a.Selector))
_, _ = hash.Write([]byte(a.BindType))
_, _ = hash.Write([]byte(a.BindName))
// Finalize the hash.
hashVal := hash.Sum(nil)
// Set and return the hash.
a.Hash = hashVal
return hashVal
}
// Equal performs an equality check on the two ACL binding rules. It handles
// nil objects.
func (a *ACLBindingRule) Equal(other *ACLBindingRule) bool {
if a == nil || other == nil {
return a == other
}
if len(a.Hash) == 0 {
a.SetHash()
}
if len(other.Hash) == 0 {
other.SetHash()
}
return bytes.Equal(a.Hash, other.Hash)
}
// Copy creates a deep copy of the ACL binding rule. This copy can then be
// safely modified. It handles nil objects.
func (a *ACLBindingRule) Copy() *ACLBindingRule {
if a == nil {
return nil
}
c := new(ACLBindingRule)
*c = *a
c.Hash = slices.Clone(a.Hash)
return c
}
// Stub converts the ACLBindingRule object into a ACLBindingRuleListStub
// object.
func (a *ACLBindingRule) Stub() *ACLBindingRuleListStub {
return &ACLBindingRuleListStub{
ID: a.ID,
Description: a.Description,
AuthMethod: a.AuthMethod,
Hash: a.Hash,
CreateIndex: a.CreateIndex,
ModifyIndex: a.ModifyIndex,
}
}
// ACLBindingRuleListStub is the stub object returned when performing a listing
// of ACL binding rules.
type ACLBindingRuleListStub struct {
// ID is an internally generated UUID for this role and is controlled by
// Nomad.
ID string
// Description is a human-readable, operator set description that can
// provide additional context about the binding role. This is an
// operational field.
Description string
// AuthMethod is the name of the auth method for which this rule applies
// to. This is required and the method must exist within state before the
// cluster administrator can create the rule.
AuthMethod string
// Hash is the hashed value of the binding rule and is generated using all
// fields from the full object except the create and modify times and
// indexes.
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
// ACLBindingRulesUpsertRequest is used to upsert a set of ACL binding rules.
type ACLBindingRulesUpsertRequest struct {
ACLBindingRules []*ACLBindingRule
// AllowMissingAuthMethods skips the ACL binding rule auth method link
// verification and is used by the replication process. The replication
// cannot ensure auth methods are present before ACL binding rules are
// replicated.
AllowMissingAuthMethods bool
WriteRequest
}
// ACLBindingRulesUpsertResponse is a response of the upsert ACL binding rules
// operation.
type ACLBindingRulesUpsertResponse struct {
ACLBindingRules []*ACLBindingRule
WriteMeta
}
// ACLBindingRulesDeleteRequest is used to delete a set of ACL binding rules by
// their IDs.
type ACLBindingRulesDeleteRequest struct {
ACLBindingRuleIDs []string
WriteRequest
}
// ACLBindingRulesDeleteResponse is a response of the delete ACL binding rules
// operation.
type ACLBindingRulesDeleteResponse struct {
WriteMeta
}
// ACLBindingRulesListRequest is the request object when performing ACL
// binding rules listings.
type ACLBindingRulesListRequest struct {
QueryOptions
}
// ACLBindingRulesListResponse is the response object when performing ACL
// binding rule listings.
type ACLBindingRulesListResponse struct {
ACLBindingRules []*ACLBindingRuleListStub
QueryMeta
}
// ACLBindingRulesRequest is the request object when performing a lookup of
// multiple binding rules by the ID.
type ACLBindingRulesRequest struct {
ACLBindingRuleIDs []string
QueryOptions
}
// ACLBindingRulesResponse is the response object when performing a lookup of
// multiple binding rules by their IDs.
type ACLBindingRulesResponse struct {
ACLBindingRules map[string]*ACLBindingRule
QueryMeta
}
// ACLBindingRuleRequest is the request object to perform a lookup of an ACL
// binding rule using a specific ID.
type ACLBindingRuleRequest struct {
ACLBindingRuleID string
QueryOptions
}
// ACLBindingRuleResponse is the response object when performing a lookup of an
// ACL binding rule matching a specific ID.
type ACLBindingRuleResponse struct {
ACLBindingRule *ACLBindingRule
QueryMeta
}
// ACLOIDCAuthURLRequest is the request to make when starting the OIDC
// authentication login flow.
type ACLOIDCAuthURLRequest struct {
// AuthMethodName is the OIDC auth-method to use. This is a required
// parameter.
AuthMethodName string
// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
RedirectURI string
// ClientNonce is a randomly generated string to prevent replay attacks. It
// is up to the client to generate this and Go integrations should use the
// oidc.NewID function within the hashicorp/cap library. This must then be
// passed back to ACLOIDCCompleteAuthRequest. This is a required parameter.
ClientNonce string
// WriteRequest is used due to the requirement by the RPC forwarding
// mechanism. This request doesn't write anything to Nomad's internal
// state.
WriteRequest
}
// Validate ensures the request object contains all the required fields in
// order to start the OIDC authentication flow.
func (a *ACLOIDCAuthURLRequest) Validate() error {
var mErr multierror.Error
if a.AuthMethodName == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
}
if a.ClientNonce == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing client nonce"))
}
if a.RedirectURI == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing redirect URI"))
}
return mErr.ErrorOrNil()
}
// ACLOIDCAuthURLResponse is the response when starting the OIDC authentication
// login flow.
type ACLOIDCAuthURLResponse struct {
// AuthURL is URL to begin authorization and is where the user logging in
// should go.
AuthURL string
}
// ACLOIDCCompleteAuthRequest is the request object to begin completing the
// OIDC auth cycle after receiving the callback from the OIDC provider.
type ACLOIDCCompleteAuthRequest struct {
// AuthMethodName is the name of the auth method being used to login via
// OIDC. This will match ACLOIDCAuthURLRequest.AuthMethodName. This is a
// required parameter.
AuthMethodName string
// ClientNonce, State, and Code are provided from the parameters given to
// the redirect URL. These are all required parameters.
ClientNonce string
State string
Code string
// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
RedirectURI string
WriteRequest
}
// Validate ensures the request object contains all the required fields in
// order to complete the OIDC authentication flow.
func (a *ACLOIDCCompleteAuthRequest) Validate() error {
var mErr multierror.Error
if a.AuthMethodName == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
}
if a.ClientNonce == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing client nonce"))
}
if a.State == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing state"))
}
if a.Code == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing code"))
}
if a.RedirectURI == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing redirect URI"))
}
return mErr.ErrorOrNil()
}
// ACLOIDCCompleteAuthResponse is the response when the OIDC auth flow has been
// completed successfully.
type ACLOIDCCompleteAuthResponse struct {
ACLToken *ACLToken
WriteMeta
}