Files
t3mp3st/bench/cve-zero/samples/GHSA-hx53-77qj-8663/source/nomad/auth/auth.go
T

958 lines
29 KiB
Go

// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"crypto/x509"
"errors"
"fmt"
"net"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/hashicorp/go-hclog"
metrics "github.com/hashicorp/go-metrics/compat"
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)
// aclCacheSize is the number of ACL objects to keep cached. ACLs have a parsing
// and construction cost, so we keep the hot objects cached to reduce the ACL
// token resolution time.
const aclCacheSize = 512
type StateGetter func() *state.StateStore
type LeaderACLGetter func() string
type RPCContext interface {
IsTLS() bool
IsStatic() bool
Certificate() *x509.Certificate
GetRemoteIP() (net.IP, error)
}
type Encrypter interface {
VerifyClaim(string) (*structs.IdentityClaims, error)
}
type Authenticator struct {
aclsEnabled bool
// verifyTLS is used to determine whether the server should verify TLS and
// is an atomic bool, so that the server TLS reload can update it at runtime
// with a race condition.
verifyTLS *atomic.Bool
logger hclog.Logger
getState StateGetter
getLeaderACL LeaderACLGetter
region string
validServerCertNames []string
validClientCertNames []string
// aclCache is used to maintain the parsed ACL objects
aclCache *structs.ACLCache[*acl.ACL]
// encrypter is a pointer to the server's Encrypter that can be used to
// verify claims
encrypter Encrypter
}
type AuthenticatorConfig struct {
StateFn StateGetter
Logger hclog.Logger
GetLeaderACLFn LeaderACLGetter
AclsEnabled bool
VerifyTLS bool
Region string
Encrypter Encrypter
}
func NewAuthenticator(cfg *AuthenticatorConfig) *Authenticator {
a := Authenticator{
aclsEnabled: cfg.AclsEnabled,
verifyTLS: &atomic.Bool{},
logger: cfg.Logger.With("auth"),
getState: cfg.StateFn,
getLeaderACL: cfg.GetLeaderACLFn,
region: cfg.Region,
aclCache: structs.NewACLCache[*acl.ACL](aclCacheSize),
encrypter: cfg.Encrypter,
validServerCertNames: []string{"server." + cfg.Region + ".nomad"},
validClientCertNames: []string{
"client." + cfg.Region + ".nomad",
"server." + cfg.Region + ".nomad",
},
}
a.verifyTLS.Store(cfg.VerifyTLS)
return &a
}
// SetVerifyTLS is a helper method to set the verifyTLS field. This is used in
// when the server TLS configuration is updated.
func (s *Authenticator) SetVerifyTLS(verifyTLS bool) { s.verifyTLS.Store(verifyTLS) }
// Authenticate extracts an AuthenticatedIdentity from the request context or
// provided token and sets the identity on the request. The caller can extract
// an acl.ACL, WorkloadIdentity, or other identifying tokens to use for
// authorization. Keeping these fields independent rather than merging them into
// an ephemeral ACLToken makes the original of the credential clear to RPC
// handlers, who may have different behavior for internal vs external origins.
//
// Note: when making a server-to-server RPC that authenticates with this method,
// the RPC *must* include the leader's ACL token. Use AuthenticateServerOnly for
// requests that don't have access to the leader's ACL token.
//
// Note: when called on the follower we'll be making stale queries, so it's
// possible if the follower is behind that the leader will get a different value
// if an ACL token or allocation's WI has just been created.
//
// This method returns errors that are used for testing diagnostics. RPC callers
// should always return ErrPermissionDenied after checking forwarding when one
// of these errors is received.
func (s *Authenticator) Authenticate(ctx RPCContext, args structs.RequestWithIdentity) error {
// get the user ACLToken or anonymous token
secretID := args.GetAuthToken()
aclToken, err := s.resolveSecretToken(secretID)
switch {
case err == nil && (aclToken == structs.AnonymousACLToken ||
aclToken == structs.ACLsDisabledToken):
// When ACLs are disabled or if we have an anonymous token, we want to
// continue on to check mTLS certs, if available, so set the token but
// don't return yet
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
case err == nil:
// ACLs are enabled and we have a non-anonymous token, so set that as
// our identity and return
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
return nil
case errors.Is(err, structs.ErrTokenExpired):
return err
case errors.Is(err, structs.ErrTokenInvalid):
// if it's not a UUID it might be an identity claim
claims, err := s.VerifyClaim(secretID)
if err != nil {
// we already know the token wasn't valid for an ACL in the state
// store, so if we get an error at this point we have an invalid
// token and there are no other options but to bail out
return err
}
args.SetIdentity(&structs.AuthenticatedIdentity{Claims: claims})
return nil
case errors.Is(err, structs.ErrTokenNotFound):
// Check if the secret ID is the leader's secret ID, in which case treat
// it as a management token.
leaderAcl := s.getLeaderACL()
if leaderAcl != "" && secretID == leaderAcl {
aclToken = structs.LeaderACLToken
break
} else {
// Otherwise, see if the secret ID belongs to a node. We should
// reach this point only on first connection.
node, err := s.getState().NodeBySecretID(nil, secretID)
if err != nil {
// this is a go-memdb error; shouldn't happen
return fmt.Errorf("could not resolve node secret: %w", err)
}
if node != nil {
args.SetIdentity(&structs.AuthenticatedIdentity{ClientID: node.ID})
return nil
}
}
// we were passed a bogus token so we'll return an error, but we'll also
// want to capture the IP for metrics
remoteIP, err := ctx.GetRemoteIP()
if err != nil {
s.logger.Error("could not determine remote address", "error", err)
}
args.SetIdentity(&structs.AuthenticatedIdentity{RemoteIP: remoteIP})
return structs.ErrPermissionDenied
default: // any other error
return fmt.Errorf("could not resolve user: %w", err)
}
// If there's no context we're in a "static" handler which only happens for
// cases where the leader is making RPCs internally (volumewatcher and
// deploymentwatcher)
if ctx.IsStatic() {
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
return nil
}
// At this point we either have an anonymous token or an invalid one.
// Unlike clients that provide their Node ID on first connection, server
// RPCs don't include an ID for the server so we identify servers by cert
// and IP address.
identity := &structs.AuthenticatedIdentity{ACLToken: aclToken}
if ctx.IsTLS() {
identity.TLSName = ctx.Certificate().Subject.CommonName
}
remoteIP, err := ctx.GetRemoteIP()
if err != nil {
s.logger.Error(
"could not authenticate RPC request or determine remote address", "error", err)
return err
}
identity.RemoteIP = remoteIP
args.SetIdentity(identity)
return nil
}
// ResolveACL is an authentication wrapper that handles resolving ACL
// tokens, Workload Identities, or client secrets into acl.ACL objects.
// Exclusively server-to-server or client-to-server requests should be using
// AuthenticateServerOnly or AuthenticateClientOnly unless they use the
// AuthenticateNodeIdentityGenerator function.
func (s *Authenticator) ResolveACL(args structs.RequestWithIdentity) (*acl.ACL, error) {
identity := args.GetIdentity()
if identity == nil {
// should never happen
return nil, structs.ErrPermissionDenied
}
if !s.aclsEnabled {
return acl.ACLsDisabledACL, nil
}
if aclObj, err := s.ResolveClientIdentityACL(identity); aclObj != nil || err != nil {
return aclObj, err
}
claims := identity.GetClaims()
if claims != nil {
return s.resolveClaims(claims)
}
// this will include any anonymous token, so this is the last chance to
// avoid an error
aclToken := identity.GetACLToken()
if aclToken != nil {
return s.resolveACLForToken(aclToken)
}
return nil, structs.ErrPermissionDenied
}
// AuthenticateServerOnly returns an ACL object for use *only* with internal
// server-to-server RPCs. This should never be used for RPCs that serve HTTP
// endpoints or accept ACL tokens to avoid confused deputy attacks by making a
// request to a follower that's forwarded.
//
// The returned ACL object is always an acl.ServerACL but in the future this
// could be extended to allow servers to have jurisdiction over specific pools,
// etc.
func (s *Authenticator) AuthenticateServerOnly(ctx RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
if err != nil {
s.logger.Error("could not determine remote address", "error", err)
}
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
defer args.SetIdentity(identity) // always set the identity, even on errors
// Note: if servers had auth tokens like clients do, we would be able to
// verify them here and only return the server ACL for actual servers even
// if mTLS was disabled. Without mTLS, any request can spoof server RPCs.
// This is known and documented in the Security Model:
// https://developer.hashicorp.com/nomad/docs/secure/acl
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validServerCertNames, identity); err != nil {
return nil, err
}
return acl.ServerACL, nil
}
// AuthenticateNodeIdentityGenerator is used for RPC endpoints (Node.Register
// and Node.UpdateStatus) that have the potential to generate node identities.
//
// While the Authenticate method serves as a complete general purpose
// authenticator, in some critical cases for identity generation checking, it
// swallows the information needed.
func (s *Authenticator) AuthenticateNodeIdentityGenerator(ctx RPCContext, args structs.RequestWithIdentity) error {
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
if err != nil {
s.logger.Error("could not determine remote address", "error", err)
}
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
defer args.SetIdentity(identity)
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validClientCertNames, identity); err != nil {
return err
}
authToken := args.GetAuthToken()
// If the auth token is empty, we treat it as an anonymous request. In the
// event of a node registration, this means the node is not yet registered.
if authToken == "" {
identity.ACLToken = structs.AnonymousACLToken
return nil
}
// If the auth token is a UUID, we check whether it's a node secret ID or
// the leader's ACL token. If it's not a UUID, we assume it's a node
// identity. Anything outside these cases is not supported and no identity
// will be set.
if helper.IsUUID(authToken) {
if leaderACL := s.getLeaderACL(); leaderACL != "" && authToken == leaderACL {
identity.ACLToken = structs.LeaderACLToken
} else {
node, err := s.getState().NodeBySecretID(nil, authToken)
if err != nil {
return fmt.Errorf("could not resolve node secret: %w", err)
}
if node == nil {
return structs.ErrPermissionDenied
}
identity.ClientID = node.ID
}
} else {
// When verifying a node identity claim, we do not want to swallow the
// initial error. This is because the caller may want to handle the
// error type in the case that the JWT is expired.
claims, err := s.VerifyClaim(authToken)
if err != nil {
return err
}
if !claims.IsNode() && !claims.IsNodeIntroduction() {
return structs.ErrPermissionDenied
}
identity.Claims = claims
}
return nil
}
// AuthenticateClientOnly returns an ACL object for use *only* with internal
// RPCs originating from clients (including those forwarded). This should never
// be used for RPCs that serve HTTP endpoints to avoid confused deputy attacks
// by making a request to a client that's forwarded. It should also not be used
// with Node.Register or NodeUpdateStatus, which should use
// AuthenticateNodeIdentityGenerator.
func (s *Authenticator) AuthenticateClientOnly(ctx RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
if err != nil {
s.logger.Error("could not determine remote address", "error", err)
}
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
defer args.SetIdentity(identity) // always set the identity, even on errors
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validClientCertNames, identity); err != nil {
return nil, err
}
authToken := args.GetAuthToken()
if authToken == "" {
return nil, structs.ErrPermissionDenied
}
// If the auth token is a UUID, we treat it as a node secret ID. Otherwise,
// we assume it's a node identity claim. Anything outside these cases is not
// permitted when using this method.
if helper.IsUUID(authToken) {
node, err := s.getState().NodeBySecretID(nil, authToken)
if err != nil {
return nil, fmt.Errorf("could not resolve node secret: %w", err)
}
if node == nil {
return nil, structs.ErrPermissionDenied
}
identity.ClientID = node.ID
} else {
claims, err := s.VerifyClaim(authToken)
if err != nil {
return nil, err
}
if !claims.IsNode() || claims.NodeIdentityClaims == nil {
return nil, structs.ErrPermissionDenied
}
identity.ClientID = claims.NodeIdentityClaims.NodeID
identity.Claims = claims
}
return s.ResolveClientIdentityACL(identity)
}
// verifyTLS is a helper function that performs TLS verification, if required,
// given the passed RPCContext and valid names.
//
// It will always set the TLSName on the identity if we are performing
// verification, so callers don't have to worry about setting it themselves.
func verifyTLS(verify bool, ctx RPCContext, validNames []string, identity *structs.AuthenticatedIdentity) error {
if verify && !ctx.IsStatic() {
tlsCert := ctx.Certificate()
if tlsCert == nil {
return errors.New("missing certificate information")
}
// Always set on the identity, even before validating the name, so we
// can capture it for metrics.
identity.TLSName = tlsCert.Subject.CommonName
// Perform the certificate validation, using the passed valid names.
_, err := validateCertificateForNames(tlsCert, validNames)
if err != nil {
return err
}
}
return nil
}
// validateCertificateForNames returns true if the certificate is valid for any
// of the given domain names.
func validateCertificateForNames(cert *x509.Certificate, expectedNames []string) (bool, error) {
if cert == nil {
return false, nil
}
validNames := []string{cert.Subject.CommonName}
validNames = append(validNames, cert.DNSNames...)
for _, expectedName := range expectedNames {
if slices.Contains(validNames, expectedName) {
return true, nil
}
}
return false, fmt.Errorf("invalid certificate: %s not in expected %s",
strings.Join(validNames, ", "),
strings.Join(expectedNames, ", "))
}
// IdentityToACLClaim returns an ACLClaim suitable for checking permissions
func IdentityToACLClaim(ai *structs.AuthenticatedIdentity, store *state.StateStore) *acl.ACLClaim {
if ai == nil || ai.Claims == nil {
return nil
}
var group string
alloc, err := store.AllocByID(nil, ai.Claims.AllocationID)
if err != nil {
// we should never hit this error, but if we did the caller would get a
// nil claim and auth will fail
return nil
}
if alloc != nil {
group = alloc.TaskGroup
}
return &acl.ACLClaim{
Namespace: ai.Claims.Namespace,
Job: ai.Claims.JobID,
Group: group,
Task: ai.Claims.TaskName,
}
}
// resolveACLForToken resolves an ACL from a token only. It should be used only
// by Variables endpoints, which have additional implicit policies for their
// claims so we can't wrap them up in ResolveACL.
func (s *Authenticator) resolveACLForToken(aclToken *structs.ACLToken) (*acl.ACL, error) {
snap, err := s.getState().Snapshot()
if err != nil {
return nil, err
}
return resolveACLFromToken(snap, s.aclCache, aclToken)
}
// ResolveToken is used to translate an ACL Token Secret ID into
// an ACL object, nil if ACLs are disabled, or an error.
//
// TODO(tgross): this is used in lots of places we probably should be calling
// Authenticate + ResolveACL on, because they support HTTP APIs that may be used
// by the Task API
func (s *Authenticator) ResolveToken(secretID string) (*acl.ACL, error) {
// Fast-path if ACLs are disabled
if !s.aclsEnabled {
return acl.ACLsDisabledACL, nil
}
defer metrics.MeasureSince([]string{"nomad", "acl", "resolveToken"}, time.Now())
// Check if the secret ID is the leader secret ID, in which case treat it as
// a management token.
if leaderAcl := s.getLeaderACL(); leaderAcl != "" && secretID == leaderAcl {
return acl.ManagementACL, nil
}
// Snapshot the state
snap, err := s.getState().Snapshot()
if err != nil {
return nil, err
}
// Resolve the ACL
return resolveTokenFromSnapshotCache(snap, s.aclCache, secretID)
}
// VerifyClaim asserts that the token is valid. If it is for a workload
// identity, it will ensure that the resulting allocation ID belongs to a
// non-terminal allocation. If the token is for a node identity, it will ensure
// the node ID matches the claim.
//
// This should usually not be called by RPC handlers.
func (s *Authenticator) VerifyClaim(token string) (*structs.IdentityClaims, error) {
claims, err := s.encrypter.VerifyClaim(token)
if err != nil {
return nil, err
}
if claims.IsWorkload() {
if err := s.verifyWorkloadIdentityClaim(claims); err != nil {
return nil, err
}
return claims, nil
}
// If the claims are for a node identity, we can return them directly once
// we have verified the claim. In the happy path, we could read the node out
// of state and verify that it is found. However, it is possible the node
// has been garbage collected, and if we failed on that check, the node
// would not be able to register again without manual intervention.
if claims.IsNode() {
return claims, nil
}
// Node introduction claims are a case where we don't verify them against
// the state store, since they are used to introduce a node that does not
// yet exist.
if claims.IsNodeIntroduction() {
return claims, nil
}
return nil, errors.New("failed to determine claim type")
}
// ResolveClientIdentityACL resolves a client ACL from an authenticated node
// identity or client secret based identity. It returns (nil, nil) if the
// identity does not represent a client.
func (s *Authenticator) ResolveClientIdentityACL(identity *structs.AuthenticatedIdentity) (*acl.ACL, error) {
if identity == nil || identity.ClientID == "" {
return nil, nil
}
if claims := identity.GetClaims(); claims != nil {
if !claims.IsNode() || claims.NodeIdentityClaims == nil || claims.NodeIdentityClaims.NodePool == "" {
return nil, structs.ErrPermissionDenied
}
return acl.NewClientACL(claims.NodeIdentityClaims.NodePool), nil
}
snap, err := s.getState().Snapshot()
if err != nil {
return nil, structs.ErrPermissionDenied
}
node, err := snap.NodeByID(nil, identity.ClientID)
if err != nil {
return nil, err
}
if node == nil {
return nil, structs.ErrPermissionDenied
}
return acl.NewClientACL(node.NodePool), nil
}
// AuthenticatedNodeID returns the authenticated node ID for client and node
// identity based requests.
func AuthenticatedNodeID(identity *structs.AuthenticatedIdentity) string {
if identity == nil {
return ""
}
if identity.ClientID != "" {
return identity.ClientID
}
claims := identity.GetClaims()
if claims != nil && claims.IsNode() && claims.NodeIdentityClaims != nil {
return claims.NodeIdentityClaims.NodeID
}
return ""
}
// AuthorizeSameNode returns ErrPermissionDenied unless the authenticated
// identity represents the same node as targetNodeID.
func AuthorizeSameNode(identity *structs.AuthenticatedIdentity, targetNodeID string) error {
if AuthenticatedNodeID(identity) != targetNodeID {
return structs.ErrPermissionDenied
}
return nil
}
// AuthorizeSameNodeServiceRegistrations returns ErrPermissionDenied unless the
// authenticated identity represents the same node as every service
// registration's NodeID.
func AuthorizeSameNodeServiceRegistrations(
identity *structs.AuthenticatedIdentity,
services []*structs.ServiceRegistration,
) error {
for _, service := range services {
if service == nil {
return structs.ErrPermissionDenied
}
if err := AuthorizeSameNode(identity, service.NodeID); err != nil {
return err
}
}
return nil
}
// ResolveAuthorizedClientNodePoolByNodeID resolves the node pool for nodeID
func resolveAuthorizedClientNodePoolByNodeID(snap *state.StateSnapshot, aclObj *acl.ACL, nodeID string) (string, error) {
if nodeID == "" {
return "", nil
}
pool, ok, err := snap.NodePoolByNodeID(nil, nodeID)
if err != nil {
return "", err
}
if !ok {
return "", nil
}
if !aclObj.AllowClientOp(pool) {
return "", structs.ErrPermissionDenied
}
return pool, nil
}
// ResolveAuthorizedClientNodePoolByNodeID resolves the node pool for nodeID
// and returns it if aclObj is authorized for that pool. It returns an empty
// pool and nil error if the node does not exist yet, which allows callers to
// continue handling blocking-query flows that may legitimately observe a
// missing node.
func (s *Authenticator) ResolveAuthorizedClientNodePoolByNodeID(aclObj *acl.ACL, nodeID string) (string, error) {
snap, err := s.getState().Snapshot()
if err != nil {
return "", err
}
return resolveAuthorizedClientNodePoolByNodeID(snap, aclObj, nodeID)
}
// AuthorizeClientAllocation returns ErrPermissionDenied unless aclObj is
// authorized for alloc's node pool. If allowNsOp is provided, callers may
// fall back to namespace-based authorization when client-scoped authorization
// does not apply.
func (s *Authenticator) AuthorizeClientAllocation(
aclObj *acl.ACL,
alloc *structs.Allocation,
allowNsOp func(*acl.ACL, string) bool,
) error {
if alloc == nil || alloc.Job == nil {
return structs.ErrPermissionDenied
}
if aclObj.AllowClientOp(alloc.Job.NodePool) {
return nil
}
if allowNsOp != nil && allowNsOp(aclObj, alloc.Namespace) {
return nil
}
return structs.ErrPermissionDenied
}
// ResolveAuthorizedClientNodePoolByServiceRegistrationID resolves the node pool
// for the node backing a service registration and returns it if aclObj is
// authorized for that pool.
func (s *Authenticator) ResolveAuthorizedClientNodePoolByServiceRegistrationID(
aclObj *acl.ACL,
namespace, id string,
) (string, error) {
if aclObj == nil || !(aclObj.AllowAgentRead() || aclObj.AllowServerOp() || aclObj.AllowNodeRead()) {
return "", structs.ErrPermissionDenied
}
snap, err := s.getState().Snapshot()
if err != nil {
return "", err
}
registration, err := snap.GetServiceRegistrationByID(nil, namespace, id)
if err != nil {
return "", err
}
if registration == nil {
return "", structs.ErrPermissionDenied
}
return resolveAuthorizedClientNodePoolByNodeID(snap, aclObj, registration.NodeID)
}
func (s *Authenticator) verifyWorkloadIdentityClaim(claims *structs.IdentityClaims) error {
snap, err := s.getState().Snapshot()
if err != nil {
return err
}
alloc, err := snap.AllocByID(nil, claims.AllocationID)
if err != nil {
return err
}
if alloc == nil || alloc.Job == nil {
return fmt.Errorf("allocation does not exist")
}
// the claims for terminal allocs are always treated as expired
if alloc.ClientTerminalStatus() {
return fmt.Errorf("allocation is terminal")
}
return nil
}
func (s *Authenticator) resolveClaims(claims *structs.IdentityClaims) (*acl.ACL, error) {
// Nomad node identity claims currently map to a client ACL. If we open this
// up in the future, we will want to modify this section to perform similar
// work that is done for workload claims.
if claims.IsNode() {
if claims.NodeIdentityClaims == nil || claims.NodeIdentityClaims.NodePool == "" {
return nil, fmt.Errorf("node identity claims missing node pool")
}
return acl.NewClientACL(claims.NodeIdentityClaims.NodePool), nil
}
policies, err := s.ResolvePoliciesForClaims(claims)
if err != nil {
return nil, err
}
// Compile and cache the ACL object. For many claims this will result in an
// ACL object with no policies, which can be efficiently cached.
aclObj, err := structs.CompileACLObject(s.aclCache, policies)
if err != nil {
return nil, err
}
return aclObj, nil
}
// resolveTokenFromSnapshotCache is used to resolve an ACL object from a
// snapshot of state, using a cache to avoid parsing and ACL construction when
// possible. It is split from resolveToken to simplify testing.
func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *structs.ACLCache[*acl.ACL], secretID string) (*acl.ACL, error) {
// Lookup the ACL Token
var token *structs.ACLToken
var err error
// Handle anonymous requests
if secretID == "" {
token = structs.AnonymousACLToken
} else {
token, err = snap.ACLTokenBySecretID(nil, secretID)
if err != nil {
return nil, err
}
if token == nil {
return nil, structs.ErrTokenNotFound
}
if token.IsExpired(time.Now().UTC()) {
return nil, structs.ErrTokenExpired
}
}
return resolveACLFromToken(snap, cache, token)
}
func resolveACLFromToken(snap *state.StateSnapshot, cache *structs.ACLCache[*acl.ACL], token *structs.ACLToken) (*acl.ACL, error) {
// Check if this is a management token
if token.Type == structs.ACLManagementToken {
return acl.ManagementACL, nil
}
// Store all policies detailed in the token request, this includes the
// named policies and those referenced within the role link.
policies := make([]*structs.ACLPolicy, 0, len(token.Policies)+len(token.Roles))
// Iterate all the token policies and add these to our policy tracking
// array.
for _, policyName := range token.Policies {
policy, err := snap.ACLPolicyByName(nil, policyName)
if err != nil {
return nil, err
}
if policy == nil {
// Ignore policies that don't exist, since they don't grant any
// more privilege.
continue
}
// Add the policy to the tracking array.
policies = append(policies, policy)
}
// Iterate all the token role links, so we can unpack these and identify
// the ACL policies.
for _, roleLink := range token.Roles {
// Any error reading the role means we cannot move forward. We just
// ignore any roles that have been detailed but are not within our
// state.
role, err := snap.GetACLRoleByID(nil, roleLink.ID)
if err != nil {
return nil, err
}
if role == nil {
continue
}
// Unpack the policies held within the ACL role to form a single list
// of ACL policies that this token has available.
for _, policyLink := range role.Policies {
policy, err := snap.ACLPolicyByName(nil, policyLink.Name)
if err != nil {
return nil, err
}
// Ignore policies that don't exist, since they don't grant any
// more privilege.
if policy == nil {
continue
}
// Add the policy to the tracking array.
policies = append(policies, policy)
}
}
// Compile and cache the ACL object
aclObj, err := structs.CompileACLObject(cache, policies)
if err != nil {
return nil, err
}
return aclObj, nil
}
// resolveSecretToken is used to translate an ACL Token Secret ID into a
// Accessor ID, the anonymous accessor, or an error.
func (s *Authenticator) resolveSecretToken(secretID string) (*structs.ACLToken, error) {
defer metrics.MeasureSince([]string{"nomad", "acl", "accessorForSecretToken"}, time.Now())
if !s.aclsEnabled {
return structs.ACLsDisabledToken, nil
}
if secretID == "" {
return structs.AnonymousACLToken, nil
}
if !helper.IsUUID(secretID) {
return nil, structs.ErrTokenInvalid
}
snap, err := s.getState().Snapshot()
if err != nil {
return nil, err
}
// Lookup the ACL Token
token, err := snap.ACLTokenBySecretID(nil, secretID)
if err != nil {
return nil, err
}
if token == nil {
return nil, structs.ErrTokenNotFound
}
if token.IsExpired(time.Now().UTC()) {
return nil, structs.ErrTokenExpired
}
return token, nil
}
func (s *Authenticator) ResolvePoliciesForClaims(claims *structs.IdentityClaims) ([]*structs.ACLPolicy, error) {
snap, err := s.getState().Snapshot()
if err != nil {
return nil, err
}
alloc, err := snap.AllocByID(nil, claims.AllocationID)
if err != nil {
return nil, err
}
if alloc == nil || alloc.Job == nil {
return nil, fmt.Errorf("allocation does not exist")
}
// Find any policies attached to the job
jobId := alloc.Job.GetIDforWorkloadIdentity()
iter, err := snap.ACLPolicyByJob(nil, alloc.Namespace, jobId)
if err != nil {
return nil, err
}
policies := []*structs.ACLPolicy{}
for {
raw := iter.Next()
if raw == nil {
break
}
policy := raw.(*structs.ACLPolicy)
if policy.JobACL == nil {
continue
}
switch {
case policy.JobACL.Group == "":
policies = append(policies, policy)
case policy.JobACL.Group != alloc.TaskGroup:
continue // don't bother checking task
case policy.JobACL.Task == "":
policies = append(policies, policy)
case policy.JobACL.Task == claims.TaskName:
policies = append(policies, policy)
}
}
iter, err = snap.ACLPolicyByNamespace(nil, alloc.Namespace)
if err != nil {
return nil, err
}
for {
raw := iter.Next()
if raw == nil {
break
}
policy := raw.(*structs.ACLPolicy)
if policy.JobACL == nil {
continue
}
policies = append(policies, policy)
}
return policies, nil
}