Files
nomad/nomad/structs/acl.go
James Rasell 62f1dbebfb server: Add RPC and HTTP functionality for node intro token gen. (#26320)
The node introduction workflow will utilise JWT's that can be used
as authentication tokens on initial client registration. This
change implements the basic builder for this JWT claim type and
the RPC and HTTP handler functionality that will expose this to
the operator.
2025-07-23 14:32:26 +01:00

2056 lines
61 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"maps"
"path"
"regexp"
"slices"
"strconv"
"time"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-set/v3"
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"
"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"
// ACLLoginRPCMethod is the RPC method for performing a non-OIDC login
// workflow. It exchanges the provided token for a Nomad ACL token with
// roles as defined within the remote provider.
//
// Args: ACLLoginRequest
// Reply: ACLLoginResponse
ACLLoginRPCMethod = "ACL.Login"
// ACLCreateClientIntroductionTokenRPCMethod is the RPC method for
// generating a client introduction token. This token is used by Nomad
// clients as an authentication token when first registering with the
// cluster.
//
// Args: ACLCreateClientIntroductionTokenRequest
// Reply: ACLCreateClientIntroductionTokenResponse
ACLCreateClientIntroductionTokenRPCMethod = "ACL.CreateClientIntroductionToken"
)
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
// ACLAuthMethodTokenLocalityLocal is the ACLAuthMethod.TokenLocality that
// will generate ACL tokens which can only be used on the local cluster the
// request was made.
ACLAuthMethodTokenLocalityLocal = "local"
// ACLAuthMethodTokenLocalityGlobal is the ACLAuthMethod.TokenLocality that
// will generate ACL tokens which can be used on all federated clusters.
ACLAuthMethodTokenLocalityGlobal = "global"
// ACLAuthMethodTypeOIDC the ACLAuthMethod.Type and represents an
// auth-method which uses the OIDC protocol.
ACLAuthMethodTypeOIDC = "OIDC"
// ACLAuthMethodTypeJWT the ACLAuthMethod.Type and represents an auth-method
// which uses the JWT type.
ACLAuthMethodTypeJWT = "JWT"
DefaultACLAuthMethodTokenNameFormat = "${auth_method_type}-${auth_method_name}"
)
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}$")
// ValidACLAuthMethodTypes lists supported auth method types.
ValidACLAuthMethodTypes = []string{ACLAuthMethodTypeOIDC, ACLAuthMethodTypeJWT}
)
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 a.ExpirationTime != nil {
if !existing.ExpirationTime.Equal(*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
}
func (a *ACLToken) Sanitize() *ACLToken {
if a == nil {
return nil
}
out := a.Copy()
out.SecretID = ""
return out
}
// 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?
TokenNameFormat string
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.TokenNameFormat))
_, _ = hash.Write([]byte(a.MaxTokenTTL.String()))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Default)))
if a.Config != nil {
_, _ = hash.Write([]byte(a.Config.JWKSURL))
_, _ = hash.Write([]byte(a.Config.JWKSCACert))
_, _ = hash.Write([]byte(a.Config.OIDCDiscoveryURL))
_, _ = hash.Write([]byte(a.Config.OIDCClientID))
_, _ = hash.Write([]byte(a.Config.OIDCClientSecret))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.OIDCEnablePKCE)))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.OIDCDisableUserInfo)))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.VerboseLogging)))
_, _ = hash.Write([]byte(a.Config.ExpirationLeeway.String()))
_, _ = hash.Write([]byte(a.Config.NotBeforeLeeway.String()))
_, _ = hash.Write([]byte(a.Config.ClockSkewLeeway.String()))
for _, ba := range a.Config.BoundAudiences {
_, _ = hash.Write([]byte(ba))
}
for _, bi := range a.Config.BoundIssuer {
_, _ = hash.Write([]byte(bi))
}
for _, uri := range a.Config.AllowedRedirectURIs {
_, _ = hash.Write([]byte(uri))
}
for _, pem := range a.Config.DiscoveryCaPem {
_, _ = hash.Write([]byte(pem))
}
for _, scope := range a.Config.OIDCScopes {
_, _ = hash.Write([]byte(scope))
}
for _, sa := range a.Config.SigningAlgs {
_, _ = hash.Write([]byte(sa))
}
for _, key := range a.Config.JWTValidationPubKeys {
_, _ = hash.Write([]byte(key))
}
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))
}
if a.Config.OIDCClientAssertion != nil {
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.KeySource))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.KeyAlgorithm))
for _, aud := range a.Config.OIDCClientAssertion.Audience {
_, _ = hash.Write([]byte(aud))
}
for k, v := range a.Config.OIDCClientAssertion.ExtraHeaders {
_, _ = hash.Write([]byte(k))
_, _ = hash.Write([]byte(v))
}
if a.Config.OIDCClientAssertion.PrivateKey != nil {
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.KeyIDHeader))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.KeyID))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemKey))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemKeyFile))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemCert))
_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemCertFile))
}
}
}
// 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
if a.TokenNameFormat == "" {
a.TokenNameFormat = DefaultACLAuthMethodTokenNameFormat
}
a.Config.Canonicalize()
}
// 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.TokenNameFormat = helper.Merge(a.TokenNameFormat, b.TokenNameFormat)
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{ACLAuthMethodTokenLocalityLocal, ACLAuthMethodTokenLocalityGlobal}, a.TokenLocality) {
mErr.Errors = append(
mErr.Errors, fmt.Errorf("invalid token locality '%s'", a.TokenLocality))
}
if !slices.Contains(ValidACLAuthMethodTypes, a.Type) {
mErr.Errors = append(
mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
}
if err := a.Config.Validate(a.Type); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid config: %w", err))
}
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()
}
// Sanitize returns a copy of the ACLAuthMethod with any secrets redacted
func (a *ACLAuthMethod) Sanitize() *ACLAuthMethod {
if a == nil || a.Config == nil {
return a
}
// copy to ensure we do not mutate a pointer pulled directly out of state.
clean := a.Copy()
// clean nested structs here, so it's obvious what all is being cleaned
// in one spot, rather than follow a stack of sanitization calls.
if clean.Config.OIDCClientSecret != "" {
clean.Config.OIDCClientSecret = "redacted"
}
if clean.Config.OIDCClientAssertion != nil {
// this ClientSecret gets inherited by the above one
if clean.Config.OIDCClientAssertion.ClientSecret != "" {
clean.Config.OIDCClientAssertion.ClientSecret = "redacted"
}
if clean.Config.OIDCClientAssertion.PrivateKey != nil &&
clean.Config.OIDCClientAssertion.PrivateKey.PemKey != "" {
clean.Config.OIDCClientAssertion.PrivateKey.PemKey = "redacted"
}
}
return clean
}
// TokenLocalityIsGlobal returns whether the auth method creates global ACL
// tokens or not.
func (a *ACLAuthMethod) TokenLocalityIsGlobal() bool {
return a.TokenLocality == ACLAuthMethodTokenLocalityGlobal
}
// ACLAuthMethodConfig is used to store configuration of an auth method
type ACLAuthMethodConfig struct {
// A list of PEM-encoded public keys to use to authenticate signatures
// locally
JWTValidationPubKeys []string
// JSON Web Key Sets url for authenticating signatures
JWKSURL string
// The OIDC Discovery URL, without any .well-known component (base path)
OIDCDiscoveryURL string
// The OAuth Client ID configured with the OIDC provider
OIDCClientID string
// The OAuth Client Secret configured with the OIDC provider
OIDCClientSecret string
// Optional client assertion ("private key jwt") config
OIDCClientAssertion *OIDCClientAssertion
// Enable PKCE challenge verification
OIDCEnablePKCE bool
// Disable claims from the OIDC UserInfo endpoint
OIDCDisableUserInfo bool
// List of OIDC scopes
OIDCScopes []string
// List of auth claims that are valid for login
BoundAudiences []string
// The value against which to match the iss claim in a JWT
BoundIssuer []string
// A list of allowed values for redirect_uri
AllowedRedirectURIs []string
// PEM encoded CA certs for use by the TLS client used to talk with the
// OIDC Discovery URL.
DiscoveryCaPem []string
// PEM encoded CA cert for use by the TLS client used to talk with the JWKS
// URL
JWKSCACert string
// A list of supported signing algorithms
SigningAlgs []string
// Duration in seconds of leeway when validating expiration of a token to
// account for clock skew
ExpirationLeeway time.Duration
// Duration in seconds of leeway when validating not before values of a
// token to account for clock skew.
NotBeforeLeeway time.Duration
// Duration in seconds of leeway when validating all claims to account for
// clock skew.
ClockSkewLeeway time.Duration
// Mappings of claims (key) that will be copied to a metadata field
// (value).
ClaimMappings map[string]string
ListClaimMappings map[string]string
// Enables logging of claims and binding-rule evaluations when
// debug level logging is enabled.
VerboseLogging bool
}
func (a *ACLAuthMethodConfig) Canonicalize() {
if a == nil {
return
}
if a.OIDCClientAssertion != nil {
// client assertions inherit certain values from auth method
if len(a.OIDCClientAssertion.Audience) == 0 {
a.OIDCClientAssertion.Audience = []string{a.OIDCDiscoveryURL}
}
// the client assertion inherits the client secret,
// in case KeySource = "client_secret"
a.OIDCClientAssertion.ClientSecret = a.OIDCClientSecret
a.OIDCClientAssertion.Canonicalize()
}
}
func (a *ACLAuthMethodConfig) Validate(methodType string) error {
if a == nil {
return errors.New("missing auth method Config")
}
mErr := &multierror.Error{}
switch methodType {
case ACLAuthMethodTypeOIDC:
if a.OIDCDiscoveryURL == "" {
mErr = multierror.Append(mErr, errors.New("missing OIDCDiscoveryURL"))
}
if a.OIDCClientID == "" {
mErr = multierror.Append(mErr, errors.New("missing OIDCClientID"))
}
if err := a.OIDCClientAssertion.Validate(); err != nil {
mErr = multierror.Append(mErr, fmt.Errorf("invalid client assertion config: %w", err))
}
case ACLAuthMethodTypeJWT:
if a.OIDCDiscoveryURL == "" && a.JWKSURL == "" && len(a.JWTValidationPubKeys) == 0 {
mErr = multierror.Append(mErr, errors.New(
"JWT auth method requires either OIDCDiscoveryURL, or JWKS URL, or JWTValidationPubKeys set"),
)
}
}
return helper.FlattenMultierror(mErr)
}
func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
if a == nil {
return nil
}
c := new(ACLAuthMethodConfig)
*c = *a
c.JWTValidationPubKeys = slices.Clone(a.JWTValidationPubKeys)
c.OIDCScopes = slices.Clone(a.OIDCScopes)
c.BoundAudiences = slices.Clone(a.BoundAudiences)
c.BoundIssuer = slices.Clone(a.BoundIssuer)
c.AllowedRedirectURIs = slices.Clone(a.AllowedRedirectURIs)
c.DiscoveryCaPem = slices.Clone(a.DiscoveryCaPem)
c.SigningAlgs = slices.Clone(a.SigningAlgs)
c.OIDCClientAssertion = a.OIDCClientAssertion.Copy()
return c
}
// MarshalJSON implements the json.Marshaler interface and allows
// time.Diration fields to be marshaled correctly.
func (a *ACLAuthMethodConfig) MarshalJSON() ([]byte, error) {
type Alias ACLAuthMethodConfig
exported := &struct {
ExpirationLeeway string
NotBeforeLeeway string
ClockSkewLeeway string
*Alias
}{
ExpirationLeeway: a.ExpirationLeeway.String(),
NotBeforeLeeway: a.NotBeforeLeeway.String(),
ClockSkewLeeway: a.ClockSkewLeeway.String(),
Alias: (*Alias)(a),
}
if a.ExpirationLeeway == 0 {
exported.ExpirationLeeway = ""
}
if a.NotBeforeLeeway == 0 {
exported.NotBeforeLeeway = ""
}
if a.ClockSkewLeeway == 0 {
exported.ClockSkewLeeway = ""
}
return json.Marshal(exported)
}
// UnmarshalJSON implements the json.Unmarshaler interface and allows
// time.Duration fields to be unmarshalled correctly.
func (a *ACLAuthMethodConfig) UnmarshalJSON(data []byte) (err error) {
type Alias ACLAuthMethodConfig
aux := &struct {
ExpirationLeeway any
NotBeforeLeeway any
ClockSkewLeeway any
*Alias
}{
Alias: (*Alias)(a),
}
if err = json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ExpirationLeeway != nil {
switch v := aux.ExpirationLeeway.(type) {
case string:
if v != "" {
if a.ExpirationLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
a.ExpirationLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected ExpirationLeeway type: %v", v)
}
}
if aux.NotBeforeLeeway != nil {
switch v := aux.NotBeforeLeeway.(type) {
case string:
if v != "" {
if a.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
a.NotBeforeLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected NotBeforeLeeway type: %v", v)
}
}
if aux.ClockSkewLeeway != nil {
switch v := aux.ClockSkewLeeway.(type) {
case string:
if v != "" {
if a.ClockSkewLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
a.ClockSkewLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected ClockSkewLeeway type: %v", v)
}
}
return nil
}
type OIDCClientAssertionKeySource string
const (
OIDCKeySourceNomad OIDCClientAssertionKeySource = "nomad"
OIDCKeySourceClientSecret OIDCClientAssertionKeySource = "client_secret"
OIDCKeySourcePrivateKey OIDCClientAssertionKeySource = "private_key"
)
// OIDCClientAssertion (a.k.a private_key_jwt) is used to send
// a client_assertion along with an OIDC token request.
// See api.OIDCClientAssertion for full field descriptions.
type OIDCClientAssertion struct {
KeySource OIDCClientAssertionKeySource
Audience []string
PrivateKey *OIDCClientAssertionKey
ExtraHeaders map[string]string
KeyAlgorithm string
// ClientSecret here is not part of the public api; it's inherited from the
// parent ACLAuthMethodConfig struct via ACLAuthMethodConfig.Canonicalize.
// It's exported mainly so that it gets saved across msgpack in raft state.
ClientSecret string
}
func (c *OIDCClientAssertion) Copy() *OIDCClientAssertion {
if c == nil {
return nil
}
n := new(OIDCClientAssertion)
*n = *c
n.Audience = slices.Clone(c.Audience)
n.PrivateKey = c.PrivateKey.Copy()
n.ExtraHeaders = maps.Clone(c.ExtraHeaders)
return n
}
func (c *OIDCClientAssertion) Canonicalize() {
if c == nil {
return
}
// default KeyAlgorithm to "RS256" for nomad and user keys, "HS256" for client_secret
if c.KeyAlgorithm == "" {
switch c.KeySource {
case OIDCKeySourceClientSecret:
c.KeyAlgorithm = "HS256"
case OIDCKeySourceNomad, OIDCKeySourcePrivateKey:
c.KeyAlgorithm = "RS256"
}
}
c.PrivateKey.Canonicalize()
}
func (c *OIDCClientAssertion) IsSet() bool {
return c != nil && c.KeySource != ""
}
func (c *OIDCClientAssertion) Validate() error {
if c == nil {
return nil
}
if len(c.Audience) == 0 || c.Audience[0] == "" {
return errors.New("missing Audience")
}
switch c.KeySource {
case OIDCKeySourceNomad:
case OIDCKeySourcePrivateKey:
if c.PrivateKey == nil {
return errors.New("PrivateKey is required for `private_key` KeySource")
}
if err := c.PrivateKey.Validate(); err != nil {
return fmt.Errorf("invalid PrivateKey: %w", err)
}
case OIDCKeySourceClientSecret:
if c.ClientSecret == "" {
return errors.New("OIDCClientSecret is required for `client_secret` KeySource")
}
default:
return fmt.Errorf("invalid KeySource %q", c.KeySource)
}
return nil
}
type OIDCClientAssertionKeyIDHeader string
const (
OIDCClientAssertionHeaderKid OIDCClientAssertionKeyIDHeader = "kid"
OIDCClientAssertionHeaderX5t OIDCClientAssertionKeyIDHeader = "x5t"
OIDCClientAssertionHeaderX5tS256 OIDCClientAssertionKeyIDHeader = "x5t#S256"
)
// OIDCClientAssertionKey contains key material provided by users for Nomad
// to use to sign the private key JWT.
// See api.OIDCClientAssertionKey for full field descriptions.
type OIDCClientAssertionKey struct {
PemKey string
PemKeyFile string
KeyIDHeader OIDCClientAssertionKeyIDHeader
PemCert string
PemCertFile string
KeyID string
}
func (k *OIDCClientAssertionKey) Copy() *OIDCClientAssertionKey {
if k == nil {
return nil
}
n := new(OIDCClientAssertionKey)
*n = *k
return n
}
func (k *OIDCClientAssertionKey) Canonicalize() {
if k == nil {
return
}
if k.KeyIDHeader == "" {
if k.KeyID != "" {
k.KeyIDHeader = OIDCClientAssertionHeaderKid
}
if k.PemCert != "" || k.PemCertFile != "" {
k.KeyIDHeader = OIDCClientAssertionHeaderX5tS256
}
}
}
var (
ErrMissingClientAssertionKey = errors.New("missing PemKey or PemKeyFile")
ErrAmbiguousClientAssertionKey = errors.New("require only one of PemKey or PemKeyFile")
ErrMissingClientAssertionKeyID = errors.New("missing PemCert, PemCertFile, or KeyID")
ErrAmbiguousClientAssertionKeyID = errors.New("require only one of PemCert, PemCertFile, or KeyID")
ErrInvalidClientAssertionKeyPath = errors.New("invalid PemKeyFile")
ErrInvalidClientAssertionCertPath = errors.New("invalid PemCertFile")
ErrInvalidKeyIDHeader = errors.New("invalid KeyIDHeader")
)
// Validate ensures that one Key and one Cert or KeyID are provided,
// and that the key ID header is valid for the provided KeyID or cert.
func (k *OIDCClientAssertionKey) Validate() error {
if k == nil {
return nil
}
// mutually exclusive key fields
// must have key file or base64, but not both
if k.PemKey == "" && k.PemKeyFile == "" {
return ErrMissingClientAssertionKey
}
if k.PemKey != "" && k.PemKeyFile != "" {
return ErrAmbiguousClientAssertionKey
}
if k.PemKeyFile != "" {
if !path.IsAbs(k.PemKeyFile) {
return fmt.Errorf("%w: must be absolute; got: %s", ErrInvalidClientAssertionKeyPath, k.PemKeyFile)
}
}
// mutually exclusive cert fields
// must have exactly one of: cert file or base64, or keyid
if k.PemCert == "" && k.PemCertFile == "" && k.KeyID == "" {
return ErrMissingClientAssertionKeyID
}
if k.PemCert != "" && (k.PemCertFile != "" || k.KeyID != "") {
return ErrAmbiguousClientAssertionKeyID
}
if k.PemCertFile != "" && (k.PemCert != "" || k.KeyID != "") {
return ErrAmbiguousClientAssertionKeyID
}
if k.KeyID != "" && (k.PemCert != "" || k.PemCertFile != "") {
return ErrAmbiguousClientAssertionKeyID
}
if k.PemCertFile != "" {
if !path.IsAbs(k.PemCertFile) {
return fmt.Errorf("%w: must be absolute; got: %s", ErrInvalidClientAssertionCertPath, k.PemCertFile)
}
}
// only allow certain key id headers
// only "kid" for KeyID
if k.KeyID != "" && k.KeyIDHeader != OIDCClientAssertionHeaderKid {
return fmt.Errorf("%w; key header for key ID must be %q",
ErrInvalidKeyIDHeader, OIDCClientAssertionHeaderKid)
}
// only "x5t*" for certs
if k.PemCert != "" || k.PemCertFile != "" {
if k.KeyIDHeader != OIDCClientAssertionHeaderX5t && k.KeyIDHeader != OIDCClientAssertionHeaderX5tS256 {
return fmt.Errorf("%w; certificate-derived key header must be one of: %q, %q",
ErrInvalidKeyIDHeader, OIDCClientAssertionHeaderX5tS256, OIDCClientAssertionHeaderX5t)
}
}
return nil
}
// 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()
}
// ACLLoginResponse is the response when the auth flow has been
// completed successfully.
type ACLLoginResponse struct {
ACLToken *ACLToken
WriteMeta
}
// ACLLoginRequest is the request object to begin auth with an external
// token provider.
type ACLLoginRequest struct {
// AuthMethodName is the name of the auth method being used to login. This
// is a required parameter.
AuthMethodName string
// LoginToken is the 3rd party token that we use to exchange for Nomad ACL
// Token in order to authenticate. This is a required parameter.
LoginToken string
WriteRequest
}
// Validate ensures the request object contains all the required fields in
// order to complete the authentication flow.
func (a *ACLLoginRequest) Validate() error {
var mErr multierror.Error
if a.AuthMethodName == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
}
if a.LoginToken == "" {
mErr.Errors = append(mErr.Errors, errors.New("missing login token"))
}
return mErr.ErrorOrNil()
}
// ACLCreateClientIntroductionTokenRequest is the request object used within the ACL
// client introduction RPC handler. This is used to generate a JWT token that
// can be used to register a new client node into the cluster.
type ACLCreateClientIntroductionTokenRequest struct {
// TTL is the requested TTL for the identity token. This is an optional
// parameter and if not set, defaults to the server defined default TTL.
TTL time.Duration
// NodeName is the name of the node that is being introduced. This is added
// to the token as a claim when present, but is optional.
NodeName string
// NodePool is the name of the node pool that this node belongs to. This is
// an optional parameter, and if not set, defaults to "default".
NodePool string
WriteRequest
}
// Canonicalize performs basic canonicalization on the ACL client introduction
// request object. This should be called within the RPC handler, to ensure a
// consistent experience for the user across CLI and HTTP API calls.
func (a *ACLCreateClientIntroductionTokenRequest) Canonicalize() {
if a.NodePool == "" {
a.NodePool = NodePoolDefault
}
}
// IdentityTTL returns the TTL that should be used for the identity token based
// on the request and server defaults.
func (a *ACLCreateClientIntroductionTokenRequest) IdentityTTL(
logger hclog.Logger,
serverDefault, serverMax time.Duration) time.Duration {
// If the user has not provided a TTL, we use the server default.
if a.TTL == 0 {
return serverDefault
}
// If the user has requested a TTL that is greater than the server defined
// maximum, we log a warning and use the server maximum instead. It is
// possible to return an error here, but providing a ceiling provides a
// smoother UX.
if a.TTL > serverMax {
logger.Warn(
"node introduction identity TTL request exceeds server maximum, using server maximum",
"requested_ttl", a.TTL, "server_max_ttl", serverMax,
)
return serverMax
}
return a.TTL
}
// ACLCreateClientIntroductionTokenResponse is the response object used within the ACL
// client introduction RPC handler.
type ACLCreateClientIntroductionTokenResponse struct {
// JWT is the signed identity token that can be used as an introduction
// token for a new client node to register with the Nomad cluster.
JWT string
}