// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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" // 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" ) 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" ) 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}$") // ValitACLAuthMethodTypes 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 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 !slices.Contains(ValidACLAuthMethodTypes, a.Type) { 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 { // 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 // 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 } 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) 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 } // 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() }