// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package auth import ( "errors" "fmt" "time" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-uuid" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" ) // ErrCannotWriteGlobalToken indicates that writing a token failed because // the token is global and this is a non-primary datacenter. var ErrCannotWriteGlobalToken = errors.New("Cannot upsert global tokens within this datacenter") // NewTokenWriter creates a new token writer. func NewTokenWriter(cfg TokenWriterConfig) *TokenWriter { return &TokenWriter{cfg} } // TokenWriter encapsulates the logic of writing ACL tokens to the state store // including validation, cache purging, etc. type TokenWriter struct { TokenWriterConfig } type TokenWriterConfig struct { RaftApply RaftApplyFn ACLCache ACLCache Store TokenWriterStore CheckUUID lib.UUIDCheckFunc MaxExpirationTTL time.Duration MinExpirationTTL time.Duration PrimaryDatacenter string InPrimaryDatacenter bool LocalTokensEnabled bool } type RaftApplyFn func(structs.MessageType, interface{}) (interface{}, error) //go:generate mockery --name ACLCache --inpackage type ACLCache interface { RemoveIdentityWithSecretToken(secretToken string) } type TokenWriterStore interface { ACLTokenGetByAccessor(ws memdb.WatchSet, accessorID string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLToken, error) ACLTokenGetBySecret(ws memdb.WatchSet, secretID string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLToken, error) ACLRoleGetByID(ws memdb.WatchSet, id string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error) ACLRoleGetByName(ws memdb.WatchSet, name string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error) ACLPolicyGetByID(ws memdb.WatchSet, id string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLPolicy, error) ACLPolicyGetByName(ws memdb.WatchSet, name string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLPolicy, error) ACLTokenUpsertValidateEnterprise(token *structs.ACLToken, existing *structs.ACLToken) error } // Create a new token. Setting fromLogin to true changes behavior slightly for // tokens created by login (as opposed to set manually via the API). func (w *TokenWriter) Create(token *structs.ACLToken, fromLogin bool) (*structs.ACLToken, error) { if err := w.checkCanWriteToken(token); err != nil { return nil, err } if token.AccessorID == "" { // Caller didn't provide an AccessorID, so generate one. id, err := lib.GenerateUUID(w.CheckUUID) if err != nil { return nil, fmt.Errorf("Failed to generate AccessorID: %w", err) } token.AccessorID = id } else { // Check the AccessorID is valid and not already in-use. if err := validateTokenID(token.AccessorID); err != nil { return nil, fmt.Errorf("Invalid Token: AccessorID - %w", err) } if inUse, err := w.tokenIDInUse(token.AccessorID); err != nil { return nil, fmt.Errorf("Failed to lookup ACL token: %w", err) } else if inUse { return nil, errors.New("Invalid Token: AccessorID is already in use") } } if token.SecretID == "" { // Caller didn't provide a SecretID, so generate one. id, err := lib.GenerateUUID(w.CheckUUID) if err != nil { return nil, fmt.Errorf("Failed to generate SecretID: %w", err) } token.SecretID = id } else { // Check the SecretID is valid and not already in-use. if err := validateTokenID(token.SecretID); err != nil { return nil, fmt.Errorf("Invalid Token: SecretID - %w", err) } if inUse, err := w.tokenIDInUse(token.SecretID); err != nil { return nil, fmt.Errorf("Failed to lookup ACL token: %w", err) } else if inUse { return nil, errors.New("Invalid Token: SecretID is already in use") } } token.CreateTime = time.Now() // Ensure ExpirationTTL is valid if provided. if token.ExpirationTTL < 0 { return nil, fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL) } else if token.ExpirationTTL > 0 { if token.HasExpirationTime() { return nil, errors.New("Token Expiration TTL and Expiration Time cannot both be set") } expirationTime := token.CreateTime.Add(token.ExpirationTTL) token.ExpirationTime = &expirationTime token.ExpirationTTL = 0 } if token.HasExpirationTime() { if token.ExpirationTime.Before(token.CreateTime) { return nil, errors.New("ExpirationTime cannot be before CreateTime") } expiresIn := token.ExpirationTime.Sub(token.CreateTime) if expiresIn > w.MaxExpirationTTL { return nil, fmt.Errorf("ExpirationTime cannot be more than %s in the future (was %s)", w.MaxExpirationTTL, expiresIn) } if expiresIn < w.MinExpirationTTL { return nil, fmt.Errorf("ExpirationTime cannot be less than %s in the future (was %s)", w.MinExpirationTTL, expiresIn) } } if fromLogin { if token.AuthMethod == "" { return nil, errors.New("AuthMethod field is required during login") } } else { if token.AuthMethod != "" { return nil, errors.New("AuthMethod field is disallowed outside of login") } } return w.write(token, nil, fromLogin) } // Update an existing token. func (w *TokenWriter) Update(token *structs.ACLToken) (*structs.ACLToken, error) { if err := w.checkCanWriteToken(token); err != nil { return nil, err } if _, err := uuid.ParseUUID(token.AccessorID); err != nil { return nil, errors.New("AccessorID is not a valid UUID") } // DEPRECATED (ACL-Legacy-Compat) - maybe get rid of this in the future // and instead do a ParseUUID check. New tokens will not have // secrets generated by users but rather they will always be UUIDs. // However if users just continue the upgrade cycle they may still // have tokens using secrets that are not UUIDS // The RootAuthorizer checks that the SecretID is not "allow", "deny" // or "manage" as a precaution against something accidentally using // one of these root policies by setting the secret to it. if acl.RootAuthorizer(token.SecretID) != nil { return nil, acl.PermissionDeniedError{Cause: "Cannot modify root ACL"} } _, match, err := w.Store.ACLTokenGetByAccessor(nil, token.AccessorID, nil) switch { case err != nil: return nil, fmt.Errorf("Failed acl token lookup by accessor: %w", err) case match == nil || match.IsExpired(time.Now()): return nil, fmt.Errorf("Cannot find token %q", token.AccessorID) } if token.SecretID == "" { token.SecretID = match.SecretID } else if match.SecretID != token.SecretID { return nil, errors.New("Changing a token's SecretID is not permitted") } if token.Local != match.Local { return nil, fmt.Errorf("Cannot toggle local mode of %s", token.AccessorID) } if token.AuthMethod == "" { token.AuthMethod = match.AuthMethod } else if match.AuthMethod != token.AuthMethod { return nil, fmt.Errorf("Cannot change AuthMethod of %s", token.AccessorID) } if token.ExpirationTTL != 0 { return nil, fmt.Errorf("Cannot change expiration time of %s", token.AccessorID) } if token.HasExpirationTime() { if !match.HasExpirationTime() || !match.ExpirationTime.Equal(*token.ExpirationTime) { return nil, fmt.Errorf("Cannot change expiration time of %s", token.AccessorID) } } else { token.ExpirationTime = match.ExpirationTime } token.CreateTime = match.CreateTime return w.write(token, match, false) } // Delete the ACL token with the given SecretID from the state store. func (w *TokenWriter) Delete(secretID string, fromLogout bool) error { _, token, err := w.Store.ACLTokenGetBySecret(nil, secretID, nil) switch { case err != nil: return err case token == nil: return acl.ErrNotFound case token.AuthMethod == "" && fromLogout: return fmt.Errorf("%w: token wasn't created via login", acl.ErrPermissionDenied) } if err := w.checkCanWriteToken(token); err != nil { return err } if _, err := w.RaftApply(structs.ACLTokenDeleteRequestType, &structs.ACLTokenBatchDeleteRequest{ TokenIDs: []string{token.AccessorID}, }); err != nil { return fmt.Errorf("Failed to apply token delete request: %w", err) } w.ACLCache.RemoveIdentityWithSecretToken(token.SecretID) return nil } func validateTokenID(id string) error { if structs.ACLIDReserved(id) { return fmt.Errorf("UUIDs with the prefix %q are reserved", structs.ACLReservedIDPrefix) } if _, err := uuid.ParseUUID(id); err != nil { return errors.New("not a valid UUID") } return nil } func (w *TokenWriter) checkCanWriteToken(token *structs.ACLToken) error { if !w.LocalTokensEnabled { return fmt.Errorf("Cannot upsert tokens within this datacenter") } if !w.InPrimaryDatacenter && !token.Local { return ErrCannotWriteGlobalToken } return nil } func (w *TokenWriter) tokenIDInUse(id string) (bool, error) { _, accessorMatch, err := w.Store.ACLTokenGetByAccessor(nil, id, nil) switch { case err != nil: return false, err case accessorMatch != nil: return true, nil } _, secretMatch, err := w.Store.ACLTokenGetBySecret(nil, id, nil) switch { case err != nil: return false, err case secretMatch != nil: return true, nil } return false, nil } func (w *TokenWriter) write(token, existing *structs.ACLToken, fromLogin bool) (*structs.ACLToken, error) { roles, err := w.normalizeRoleLinks(token.Roles, &token.EnterpriseMeta) if err != nil { return nil, err } token.Roles = roles policies, err := w.normalizePolicyLinks(token.Policies, &token.EnterpriseMeta) if err != nil { return nil, err } token.Policies = policies serviceIdentities, err := w.normalizeServiceIdentities(token.ServiceIdentities, token.Local) if err != nil { return nil, err } token.ServiceIdentities = serviceIdentities nodeIdentities, err := w.normalizeNodeIdentities(token.NodeIdentities) if err != nil { return nil, err } token.NodeIdentities = nodeIdentities if err := w.enterpriseValidation(token, existing); err != nil { return nil, err } token.SetHash(true) // Persist the token by writing to Raft. _, err = w.RaftApply(structs.ACLTokenSetRequestType, &structs.ACLTokenBatchSetRequest{ Tokens: structs.ACLTokens{token}, // Logins may attempt to link to roles that do not exist. These may be // persisted, but don't allow tokens to be created that have no privileges // (i.e. role links that point nowhere). AllowMissingLinks: fromLogin, ProhibitUnprivileged: fromLogin, }) if err != nil { return nil, fmt.Errorf("Failed to apply token write request: %w", err) } // Purge the token from the ACL cache. w.ACLCache.RemoveIdentityWithSecretToken(token.SecretID) // Refresh the token from the state store. _, updatedToken, err := w.Store.ACLTokenGetByAccessor(nil, token.AccessorID, nil) if err != nil || updatedToken == nil { return nil, errors.New("Failed to retrieve token after insertion") } return updatedToken, nil } func (w *TokenWriter) normalizeRoleLinks(links []structs.ACLTokenRoleLink, entMeta *acl.EnterpriseMeta) ([]structs.ACLTokenRoleLink, error) { var normalized []structs.ACLTokenRoleLink uniqueIDs := make(map[string]struct{}) for _, link := range links { if link.ID == "" { _, role, err := w.Store.ACLRoleGetByName(nil, link.Name, entMeta) switch { case err != nil: return nil, fmt.Errorf("Error looking up role for name: %q: %w", link.Name, err) case role == nil: return nil, fmt.Errorf("No such ACL role with name %q", link.Name) } link.ID = role.ID } else { _, role, err := w.Store.ACLRoleGetByID(nil, link.ID, entMeta) switch { case err != nil: return nil, fmt.Errorf("Error looking up role for ID: %q: %w", link.ID, err) case role == nil: return nil, fmt.Errorf("No such ACL role with ID %q", link.ID) } } // Do not persist the role name as the role could be renamed in the future. link.Name = "" // De-duplicate role links by ID. if _, ok := uniqueIDs[link.ID]; !ok { normalized = append(normalized, link) uniqueIDs[link.ID] = struct{}{} } } return normalized, nil } func (w *TokenWriter) normalizePolicyLinks(links []structs.ACLTokenPolicyLink, entMeta *acl.EnterpriseMeta) ([]structs.ACLTokenPolicyLink, error) { var normalized []structs.ACLTokenPolicyLink uniqueIDs := make(map[string]struct{}) for _, link := range links { if link.ID == "" { _, role, err := w.Store.ACLPolicyGetByName(nil, link.Name, entMeta) switch { case err != nil: return nil, fmt.Errorf("Error looking up policy for name: %q: %w", link.Name, err) case role == nil: return nil, fmt.Errorf("No such ACL policy with name %q", link.Name) } link.ID = role.ID } else { _, role, err := w.Store.ACLPolicyGetByID(nil, link.ID, entMeta) switch { case err != nil: return nil, fmt.Errorf("Error looking up policy for ID: %q: %w", link.ID, err) case role == nil: return nil, fmt.Errorf("No such ACL policy with ID %q", link.ID) } } // Do not persist the role name as the role could be renamed in the future. link.Name = "" // De-duplicate role links by ID. if _, ok := uniqueIDs[link.ID]; !ok { normalized = append(normalized, link) uniqueIDs[link.ID] = struct{}{} } } return normalized, nil } func (w *TokenWriter) normalizeServiceIdentities(svcIDs structs.ACLServiceIdentities, tokenLocal bool) (structs.ACLServiceIdentities, error) { for _, id := range svcIDs { if id.ServiceName == "" { return nil, errors.New("Service identity is missing the service name field on this token") } if tokenLocal && len(id.Datacenters) > 0 { return nil, fmt.Errorf("Service identity %q cannot specify a list of datacenters on a local token", id.ServiceName) } if !acl.IsValidServiceIdentityName(id.ServiceName) { return nil, fmt.Errorf("Service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", id.ServiceName) } } return svcIDs.Deduplicate(), nil } func (w *TokenWriter) normalizeNodeIdentities(nodeIDs structs.ACLNodeIdentities) (structs.ACLNodeIdentities, error) { for _, id := range nodeIDs { if id.NodeName == "" { return nil, errors.New("Node identity is missing the node name field on this token") } if id.Datacenter == "" { return nil, errors.New("Node identity is missing the datacenter field on this token") } if !acl.IsValidNodeIdentityName(id.NodeName) { return nil, fmt.Errorf("Node identity has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed") } } return nodeIDs.Deduplicate(), nil }