open-nomad/client/acl.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

355 lines
12 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2017-08-20 23:00:54 +00:00
package client
import (
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/go-set"
2017-08-20 23:00:54 +00:00
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/structs"
)
2017-08-23 20:49:08 +00:00
const (
// policyCacheSize is the number of ACL policies to keep cached. Policies have a fetching cost
// so we keep the hot policies cached to reduce the ACL token resolution time.
policyCacheSize = 64
// 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.
aclCacheSize = 64
// tokenCacheSize is the number of bearer tokens, ACL and workload identity,
// to keep cached. Tokens have a fetching cost, so we keep the hot tokens
// cached to reduce the lookups.
tokenCacheSize = 128
// roleCacheSize is the number of ACL roles to keep cached. Looking up
// roles requires an RPC call, so we keep the hot roles cached to reduce
// the number of lookups.
roleCacheSize = 64
2017-08-23 20:49:08 +00:00
)
// clientACLResolver holds the state required for client resolution
// of ACLs
type clientACLResolver struct {
// aclCache is used to maintain the parsed ACL objects
aclCache *structs.ACLCache[*acl.ACL]
2017-08-23 20:49:08 +00:00
// policyCache is used to maintain the fetched policy objects
policyCache *structs.ACLCache[*structs.ACLPolicy]
2017-08-23 20:49:08 +00:00
// tokenCache is used to maintain the fetched token objects
tokenCache *structs.ACLCache[*structs.AuthenticatedIdentity]
// roleCache is used to maintain a cache of the fetched ACL roles. Each
// entry is keyed by the role ID.
roleCache *structs.ACLCache[*structs.ACLRole]
2017-08-23 20:49:08 +00:00
}
// init is used to setup the client resolver state
func (c *clientACLResolver) init() {
c.aclCache = structs.NewACLCache[*acl.ACL](aclCacheSize)
c.policyCache = structs.NewACLCache[*structs.ACLPolicy](policyCacheSize)
c.tokenCache = structs.NewACLCache[*structs.AuthenticatedIdentity](tokenCacheSize)
c.roleCache = structs.NewACLCache[*structs.ACLRole](roleCacheSize)
2017-08-20 23:00:54 +00:00
}
// ResolveToken is used to translate an ACL Token Secret ID or workload
// identity into an ACL object, nil if ACLs are disabled, or an error.
func (c *Client) ResolveToken(bearerToken string) (*acl.ACL, error) {
a, _, err := c.resolveTokenAndACL(bearerToken)
return a, err
}
func (c *Client) resolveTokenAndACL(bearerToken string) (*acl.ACL, *structs.AuthenticatedIdentity, error) {
2017-08-20 23:00:54 +00:00
// Fast-path if ACLs are disabled
client: fix data races in config handling (#14139) Before this change, Client had 2 copies of the config object: config and configCopy. There was no guidance around which to use where (other than configCopy's comment to pass it to alloc runners), both are shared among goroutines and mutated in data racy ways. At least at one point I think the idea was to have `config` be mutable and then grab a lock to overwrite `configCopy`'s pointer atomically. This would have allowed alloc runners to read their config copies in data race safe ways, but this isn't how the current implementation worked. This change takes the following approach to safely handling configs in the client: 1. `Client.config` is the only copy of the config and all access must go through the `Client.configLock` mutex 2. Since the mutex *only protects the config pointer itself and not fields inside the Config struct:* all config mutation must be done on a *copy* of the config, and then Client's config pointer is overwritten while the mutex is acquired. Alloc runners and other goroutines with the old config pointer will not see config updates. 3. Deep copying is implemented on the Config struct to satisfy the previous approach. The TLS Keyloader is an exception because it has its own internal locking to support mutating in place. An unfortunate complication but one I couldn't find a way to untangle in a timely fashion. 4. To facilitate deep copying I made an *internally backward incompatible API change:* our `helper/funcs` used to turn containers (slices and maps) with 0 elements into nils. This probably saves a few memory allocations but makes it very easy to cause panics. Since my new config handling approach uses more copying, it became very difficult to ensure all code that used containers on configs could handle nils properly. Since this code has caused panics in the past, I fixed it: nil containers are copied as nil, but 0-element containers properly return a new 0-element container. No more "downgrading to nil!"
2022-08-18 23:32:04 +00:00
if !c.GetConfig().ACLEnabled {
return nil, nil, nil
2017-08-20 23:00:54 +00:00
}
2017-08-23 20:49:08 +00:00
defer metrics.MeasureSince([]string{"client", "acl", "resolve_token"}, time.Now())
2017-08-20 23:00:54 +00:00
// Resolve the token value
ident, err := c.resolveTokenValue(bearerToken)
2017-08-20 23:00:54 +00:00
if err != nil {
return nil, nil, err
2017-08-20 23:00:54 +00:00
}
// Only allow ACLs and workload identities to call client RPCs
if ident.ACLToken == nil && ident.Claims == nil {
return nil, nil, structs.ErrTokenNotFound
2017-08-20 23:00:54 +00:00
}
// Give the token expiry some slight leeway in case the client and server
// clocks are skewed.
if ident.IsExpired(time.Now().Add(2 * time.Second)) {
return nil, nil, structs.ErrTokenExpired
}
var policies []*structs.ACLPolicy
2017-08-20 23:00:54 +00:00
// Resolve token policies
if token := ident.ACLToken; token != nil {
// Check if this is a management token
if ident.ACLToken.Type == structs.ACLManagementToken {
return acl.ManagementACL, ident, nil
}
// Resolve the policy links within the token ACL roles.
policyNames, err := c.resolveTokenACLRoles(bearerToken, token.Roles)
if err != nil {
return nil, nil, err
}
// Generate a slice of all policy names included within the token, taken
// from both the ACL roles and the direct assignments.
policyNames = append(policyNames, token.Policies...)
// Resolve ACL token policies
if policies, err = c.resolvePolicies(token.SecretID, policyNames); err != nil {
return nil, nil, err
}
} else {
// Resolve policies for workload identities
policyArgs := structs.GenericRequest{
QueryOptions: structs.QueryOptions{
AuthToken: bearerToken,
Region: c.Region(),
},
}
policyReply := structs.ACLPolicySetResponse{}
if err := c.RPC("ACL.GetClaimPolicies", &policyArgs, &policyReply); err != nil {
return nil, nil, err
}
policies = make([]*structs.ACLPolicy, 0, len(policyReply.Policies))
for _, p := range policyReply.Policies {
policies = append(policies, p)
}
2017-08-20 23:00:54 +00:00
}
// Resolve the ACL object
aclObj, err := structs.CompileACLObject(c.aclCache, policies)
if err != nil {
return nil, nil, err
2017-08-20 23:00:54 +00:00
}
return aclObj, ident, nil
2017-08-20 23:00:54 +00:00
}
// resolveTokenValue is used to translate a bearer token, either an ACL token's
// secret or a workload identity, into an ACL token with caching We use a local
// cache up to the TTL limit, and then resolve via a server. If we cannot
2017-08-20 23:00:54 +00:00
// reach a server, but have a cached value we extend the TTL to gracefully handle outages.
func (c *Client) resolveTokenValue(bearerToken string) (*structs.AuthenticatedIdentity, error) {
2017-08-20 23:00:54 +00:00
// Hot-path the anonymous token
if bearerToken == "" {
return &structs.AuthenticatedIdentity{ACLToken: structs.AnonymousACLToken}, nil
2017-08-20 23:00:54 +00:00
}
// Lookup the token entry in the cache
entry, ok := c.tokenCache.Get(bearerToken)
2017-08-20 23:00:54 +00:00
if ok {
if entry.Age() <= c.GetConfig().ACLTokenTTL {
return entry.Get(), nil
2017-08-20 23:00:54 +00:00
}
}
// Lookup the token
req := structs.GenericRequest{
QueryOptions: structs.QueryOptions{
AuthToken: bearerToken,
Region: c.Region(),
AllowStale: true,
},
2017-08-20 23:00:54 +00:00
}
var resp structs.ACLWhoAmIResponse
if err := c.RPC("ACL.WhoAmI", &req, &resp); err != nil {
2017-08-20 23:00:54 +00:00
// If we encounter an error but have a cached value, mask the error and extend the cache
if ok {
c.logger.Warn("failed to resolve token, using expired cached value", "error", err)
return entry.Get(), nil
2017-08-20 23:00:54 +00:00
}
return nil, err
}
// Cache the response (positive or negative)
c.tokenCache.Add(bearerToken, resp.Identity)
return resp.Identity, nil
2017-08-20 23:00:54 +00:00
}
// resolvePolicies is used to translate a set of named ACL policies into the objects.
// We cache the policies locally, and fault them from a server as necessary. Policies
// are cached for a TTL, and then refreshed. If a server cannot be reached, the cache TTL
// will be ignored to gracefully handle outages.
func (c *Client) resolvePolicies(secretID string, policies []string) ([]*structs.ACLPolicy, error) {
2017-08-20 23:00:54 +00:00
var out []*structs.ACLPolicy
var expired []*structs.ACLPolicy
var missing []string
// Scan the cache for each policy
for _, policyName := range policies {
// Lookup the policy in the cache
entry, ok := c.policyCache.Get(policyName)
2017-08-20 23:00:54 +00:00
if !ok {
missing = append(missing, policyName)
continue
}
// Check if the cached value is valid or expired
if entry.Age() <= c.GetConfig().ACLPolicyTTL {
out = append(out, entry.Get())
2017-08-20 23:00:54 +00:00
} else {
expired = append(expired, entry.Get())
2017-08-20 23:00:54 +00:00
}
}
// Hot-path if we have no missing or expired policies
if len(missing)+len(expired) == 0 {
return out, nil
}
// Lookup the missing and expired policies
fetch := missing
for _, p := range expired {
fetch = append(fetch, p.Name)
}
req := structs.ACLPolicySetRequest{
Names: fetch,
QueryOptions: structs.QueryOptions{
Region: c.Region(),
2017-10-12 22:16:33 +00:00
AuthToken: secretID,
AllowStale: true,
},
2017-08-20 23:00:54 +00:00
}
var resp structs.ACLPolicySetResponse
if err := c.RPC("ACL.GetPolicies", &req, &resp); err != nil {
// If we encounter an error but have cached policies, mask the error and extend the cache
if len(missing) == 0 {
c.logger.Warn("failed to resolve policies, using expired cached value", "error", err)
2017-08-20 23:00:54 +00:00
out = append(out, expired...)
return out, nil
}
return nil, err
}
// Handle each output
for _, policy := range resp.Policies {
c.policyCache.Add(policy.Name, policy)
2017-08-20 23:00:54 +00:00
out = append(out, policy)
}
// Return the valid policies
return out, nil
}
// resolveTokenACLRoles is used to unpack an ACL roles and their policy
// assignments into a list of ACL policy names. This can then be used to
// compile an ACL object.
//
// When roles need to be looked up from state via server RPC, we may use the
// expired cache version. This can only occur if we can fully resolve the role
// via the cache.
func (c *Client) resolveTokenACLRoles(secretID string, roleLinks []*structs.ACLTokenRoleLink) ([]string, error) {
var (
// missingRoleIDs are the roles linked which are not found within our
// cache. These must be looked up from the server via and RPC, so we
// can correctly identify the policy links.
missingRoleIDs []string
// expiredRoleIDs are the roles linked which have been found within our
// cache, but are expired. These must be looked up from the server via
// and RPC, so we can correctly identify the policy links.
expiredRoleIDs []string
)
// policyNames tracks the resolved ACL policies which are linked to the
// role as a deduplicated list. This is the output object and represents
// the authorisation this role provides token bearers.
policyNames := set.New[string](0)
for _, roleLink := range roleLinks {
// Look within the cache to see if the role is already present. If we
// do not find it, add the ID to our tracking, so we look this up via
// RPC.
entry, ok := c.roleCache.Get(roleLink.ID)
if !ok {
missingRoleIDs = append(missingRoleIDs, roleLink.ID)
continue
}
// If the cached value is expired, add the ID to our tracking, so we
// look this up via RPC. Otherwise, iterate the policy links and add
// each policy name to our return object tracking.
if entry.Age() <= c.GetConfig().ACLRoleTTL {
for _, policyLink := range entry.Get().Policies {
policyNames.Insert(policyLink.Name)
}
} else {
expiredRoleIDs = append(expiredRoleIDs, entry.Get().ID)
}
}
// Hot-path: we were able to resolve all ACL roles via the cache and
// generate a list of linked policy names. Therefore, we can avoid making
// any RPC calls.
if len(missingRoleIDs)+len(expiredRoleIDs) == 0 {
return policyNames.Slice(), nil
}
// Created a combined list of role IDs that we need to lookup from server
// state.
roleIDsToFetch := missingRoleIDs
roleIDsToFetch = append(roleIDsToFetch, expiredRoleIDs...)
// Generate an RPC request to detail all the ACL roles that we did not find
// or were expired within the cache.
roleByIDReq := structs.ACLRolesByIDRequest{
ACLRoleIDs: roleIDsToFetch,
QueryOptions: structs.QueryOptions{
Region: c.Region(),
AuthToken: secretID,
AllowStale: true,
},
}
var roleByIDResp structs.ACLRolesByIDResponse
// Perform the RPC call to detail the required ACL roles. If the RPC call
// fails, and we are only updating expired cache entries, use the expired
// entries. This allows use to handle intermittent failures.
err := c.RPC(structs.ACLGetRolesByIDRPCMethod, &roleByIDReq, &roleByIDResp)
if err != nil {
if len(missingRoleIDs) == 0 {
c.logger.Warn("failed to resolve ACL roles, using expired cached value", "error", err)
for _, aclRole := range roleByIDResp.ACLRoles {
for _, rolePolicyLink := range aclRole.Policies {
policyNames.Insert(rolePolicyLink.Name)
}
}
return policyNames.Slice(), nil
}
return nil, err
}
// Generate a timestamp for the cache entry. We do not need to use a
// timestamp per ACL role response integration.
now := time.Now()
for _, aclRole := range roleByIDResp.ACLRoles {
// Add an entry to the cache using the generated timestamp for future
// expiry calculations. Any existing, expired entry will be
// overwritten.
c.roleCache.AddAtTime(aclRole.ID, aclRole, now)
// Iterate the role policy links, extracting the name and adding this
// to our return response tracking.
for _, rolePolicyLink := range aclRole.Policies {
policyNames.Insert(rolePolicyLink.Name)
}
}
return policyNames.Slice(), nil
}