github.com/blixtra/nomad@v0.7.2-0.20171221000451-da9a1d7bb050/client/acl.go (about)

     1  package client
     2  
     3  import (
     4  	"time"
     5  
     6  	metrics "github.com/armon/go-metrics"
     7  	lru "github.com/hashicorp/golang-lru"
     8  	"github.com/hashicorp/nomad/acl"
     9  	"github.com/hashicorp/nomad/nomad/structs"
    10  )
    11  
    12  const (
    13  	// policyCacheSize is the number of ACL policies to keep cached. Policies have a fetching cost
    14  	// so we keep the hot policies cached to reduce the ACL token resolution time.
    15  	policyCacheSize = 64
    16  
    17  	// aclCacheSize is the number of ACL objects to keep cached. ACLs have a parsing and
    18  	// construction cost, so we keep the hot objects cached to reduce the ACL token resolution time.
    19  	aclCacheSize = 64
    20  
    21  	// tokenCacheSize is the number of ACL tokens to keep cached. Tokens have a fetching cost,
    22  	// so we keep the hot tokens cached to reduce the lookups.
    23  	tokenCacheSize = 64
    24  )
    25  
    26  // clientACLResolver holds the state required for client resolution
    27  // of ACLs
    28  type clientACLResolver struct {
    29  	// aclCache is used to maintain the parsed ACL objects
    30  	aclCache *lru.TwoQueueCache
    31  
    32  	// policyCache is used to maintain the fetched policy objects
    33  	policyCache *lru.TwoQueueCache
    34  
    35  	// tokenCache is used to maintain the fetched token objects
    36  	tokenCache *lru.TwoQueueCache
    37  }
    38  
    39  // init is used to setup the client resolver state
    40  func (c *clientACLResolver) init() error {
    41  	// Create the ACL object cache
    42  	var err error
    43  	c.aclCache, err = lru.New2Q(aclCacheSize)
    44  	if err != nil {
    45  		return err
    46  	}
    47  	c.policyCache, err = lru.New2Q(policyCacheSize)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	c.tokenCache, err = lru.New2Q(tokenCacheSize)
    52  	if err != nil {
    53  		return err
    54  	}
    55  	return nil
    56  }
    57  
    58  // cachedACLValue is used to manage ACL Token or Policy TTLs
    59  type cachedACLValue struct {
    60  	Token     *structs.ACLToken
    61  	Policy    *structs.ACLPolicy
    62  	CacheTime time.Time
    63  }
    64  
    65  // Age is the time since the token was cached
    66  func (c *cachedACLValue) Age() time.Duration {
    67  	return time.Since(c.CacheTime)
    68  }
    69  
    70  // ResolveToken is used to translate an ACL Token Secret ID into
    71  // an ACL object, nil if ACLs are disabled, or an error.
    72  func (c *Client) ResolveToken(secretID string) (*acl.ACL, error) {
    73  	// Fast-path if ACLs are disabled
    74  	if !c.config.ACLEnabled {
    75  		return nil, nil
    76  	}
    77  	defer metrics.MeasureSince([]string{"client", "acl", "resolve_token"}, time.Now())
    78  
    79  	// Resolve the token value
    80  	token, err := c.resolveTokenValue(secretID)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	if token == nil {
    85  		return nil, structs.ErrTokenNotFound
    86  	}
    87  
    88  	// Check if this is a management token
    89  	if token.Type == structs.ACLManagementToken {
    90  		return acl.ManagementACL, nil
    91  	}
    92  
    93  	// Resolve the policies
    94  	policies, err := c.resolvePolicies(token.SecretID, token.Policies)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// Resolve the ACL object
   100  	aclObj, err := structs.CompileACLObject(c.aclCache, policies)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return aclObj, nil
   105  }
   106  
   107  // resolveTokenValue is used to translate a secret ID into an ACL token with caching
   108  // We use a local cache up to the TTL limit, and then resolve via a server. If we cannot
   109  // reach a server, but have a cached value we extend the TTL to gracefully handle outages.
   110  func (c *Client) resolveTokenValue(secretID string) (*structs.ACLToken, error) {
   111  	// Hot-path the anonymous token
   112  	if secretID == "" {
   113  		return structs.AnonymousACLToken, nil
   114  	}
   115  
   116  	// Lookup the token in the cache
   117  	raw, ok := c.tokenCache.Get(secretID)
   118  	if ok {
   119  		cached := raw.(*cachedACLValue)
   120  		if cached.Age() <= c.config.ACLTokenTTL {
   121  			return cached.Token, nil
   122  		}
   123  	}
   124  
   125  	// Lookup the token
   126  	req := structs.ResolveACLTokenRequest{
   127  		SecretID: secretID,
   128  		QueryOptions: structs.QueryOptions{
   129  			Region:     c.Region(),
   130  			AllowStale: true,
   131  		},
   132  	}
   133  	var resp structs.ResolveACLTokenResponse
   134  	if err := c.RPC("ACL.ResolveToken", &req, &resp); err != nil {
   135  		// If we encounter an error but have a cached value, mask the error and extend the cache
   136  		if ok {
   137  			c.logger.Printf("[WARN] client: failed to resolve token, using expired cached value: %v", err)
   138  			cached := raw.(*cachedACLValue)
   139  			return cached.Token, nil
   140  		}
   141  		return nil, err
   142  	}
   143  
   144  	// Cache the response (positive or negative)
   145  	c.tokenCache.Add(secretID, &cachedACLValue{
   146  		Token:     resp.Token,
   147  		CacheTime: time.Now(),
   148  	})
   149  	return resp.Token, nil
   150  }
   151  
   152  // resolvePolicies is used to translate a set of named ACL policies into the objects.
   153  // We cache the policies locally, and fault them from a server as necessary. Policies
   154  // are cached for a TTL, and then refreshed. If a server cannot be reached, the cache TTL
   155  // will be ignored to gracefully handle outages.
   156  func (c *Client) resolvePolicies(secretID string, policies []string) ([]*structs.ACLPolicy, error) {
   157  	var out []*structs.ACLPolicy
   158  	var expired []*structs.ACLPolicy
   159  	var missing []string
   160  
   161  	// Scan the cache for each policy
   162  	for _, policyName := range policies {
   163  		// Lookup the policy in the cache
   164  		raw, ok := c.policyCache.Get(policyName)
   165  		if !ok {
   166  			missing = append(missing, policyName)
   167  			continue
   168  		}
   169  
   170  		// Check if the cached value is valid or expired
   171  		cached := raw.(*cachedACLValue)
   172  		if cached.Age() <= c.config.ACLPolicyTTL {
   173  			out = append(out, cached.Policy)
   174  		} else {
   175  			expired = append(expired, cached.Policy)
   176  		}
   177  	}
   178  
   179  	// Hot-path if we have no missing or expired policies
   180  	if len(missing)+len(expired) == 0 {
   181  		return out, nil
   182  	}
   183  
   184  	// Lookup the missing and expired policies
   185  	fetch := missing
   186  	for _, p := range expired {
   187  		fetch = append(fetch, p.Name)
   188  	}
   189  	req := structs.ACLPolicySetRequest{
   190  		Names: fetch,
   191  		QueryOptions: structs.QueryOptions{
   192  			Region:     c.Region(),
   193  			AuthToken:  secretID,
   194  			AllowStale: true,
   195  		},
   196  	}
   197  	var resp structs.ACLPolicySetResponse
   198  	if err := c.RPC("ACL.GetPolicies", &req, &resp); err != nil {
   199  		// If we encounter an error but have cached policies, mask the error and extend the cache
   200  		if len(missing) == 0 {
   201  			c.logger.Printf("[WARN] client: failed to resolve policies, using expired cached value: %v", err)
   202  			out = append(out, expired...)
   203  			return out, nil
   204  		}
   205  		return nil, err
   206  	}
   207  
   208  	// Handle each output
   209  	for _, policy := range resp.Policies {
   210  		c.policyCache.Add(policy.Name, &cachedACLValue{
   211  			Policy:    policy,
   212  			CacheTime: time.Now(),
   213  		})
   214  		out = append(out, policy)
   215  	}
   216  
   217  	// Return the valid policies
   218  	return out, nil
   219  }