github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/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 a, _, err := c.resolveTokenAndACL(secretID) 74 return a, err 75 } 76 77 func (c *Client) resolveTokenAndACL(secretID string) (*acl.ACL, *structs.ACLToken, error) { 78 // Fast-path if ACLs are disabled 79 if !c.config.ACLEnabled { 80 return nil, nil, nil 81 } 82 defer metrics.MeasureSince([]string{"client", "acl", "resolve_token"}, time.Now()) 83 84 // Resolve the token value 85 token, err := c.resolveTokenValue(secretID) 86 if err != nil { 87 return nil, nil, err 88 } 89 if token == nil { 90 return nil, nil, structs.ErrTokenNotFound 91 } 92 93 // Check if this is a management token 94 if token.Type == structs.ACLManagementToken { 95 return acl.ManagementACL, token, nil 96 } 97 98 // Resolve the policies 99 policies, err := c.resolvePolicies(token.SecretID, token.Policies) 100 if err != nil { 101 return nil, nil, err 102 } 103 104 // Resolve the ACL object 105 aclObj, err := structs.CompileACLObject(c.aclCache, policies) 106 if err != nil { 107 return nil, nil, err 108 } 109 return aclObj, token, nil 110 } 111 112 // resolveTokenValue is used to translate a secret ID into an ACL token with caching 113 // We use a local cache up to the TTL limit, and then resolve via a server. If we cannot 114 // reach a server, but have a cached value we extend the TTL to gracefully handle outages. 115 func (c *Client) resolveTokenValue(secretID string) (*structs.ACLToken, error) { 116 // Hot-path the anonymous token 117 if secretID == "" { 118 return structs.AnonymousACLToken, nil 119 } 120 121 // Lookup the token in the cache 122 raw, ok := c.tokenCache.Get(secretID) 123 if ok { 124 cached := raw.(*cachedACLValue) 125 if cached.Age() <= c.config.ACLTokenTTL { 126 return cached.Token, nil 127 } 128 } 129 130 // Lookup the token 131 req := structs.ResolveACLTokenRequest{ 132 SecretID: secretID, 133 QueryOptions: structs.QueryOptions{ 134 Region: c.Region(), 135 AllowStale: true, 136 }, 137 } 138 var resp structs.ResolveACLTokenResponse 139 if err := c.RPC("ACL.ResolveToken", &req, &resp); err != nil { 140 // If we encounter an error but have a cached value, mask the error and extend the cache 141 if ok { 142 c.logger.Warn("failed to resolve token, using expired cached value", "error", err) 143 cached := raw.(*cachedACLValue) 144 return cached.Token, nil 145 } 146 return nil, err 147 } 148 149 // Cache the response (positive or negative) 150 c.tokenCache.Add(secretID, &cachedACLValue{ 151 Token: resp.Token, 152 CacheTime: time.Now(), 153 }) 154 return resp.Token, nil 155 } 156 157 // resolvePolicies is used to translate a set of named ACL policies into the objects. 158 // We cache the policies locally, and fault them from a server as necessary. Policies 159 // are cached for a TTL, and then refreshed. If a server cannot be reached, the cache TTL 160 // will be ignored to gracefully handle outages. 161 func (c *Client) resolvePolicies(secretID string, policies []string) ([]*structs.ACLPolicy, error) { 162 var out []*structs.ACLPolicy 163 var expired []*structs.ACLPolicy 164 var missing []string 165 166 // Scan the cache for each policy 167 for _, policyName := range policies { 168 // Lookup the policy in the cache 169 raw, ok := c.policyCache.Get(policyName) 170 if !ok { 171 missing = append(missing, policyName) 172 continue 173 } 174 175 // Check if the cached value is valid or expired 176 cached := raw.(*cachedACLValue) 177 if cached.Age() <= c.config.ACLPolicyTTL { 178 out = append(out, cached.Policy) 179 } else { 180 expired = append(expired, cached.Policy) 181 } 182 } 183 184 // Hot-path if we have no missing or expired policies 185 if len(missing)+len(expired) == 0 { 186 return out, nil 187 } 188 189 // Lookup the missing and expired policies 190 fetch := missing 191 for _, p := range expired { 192 fetch = append(fetch, p.Name) 193 } 194 req := structs.ACLPolicySetRequest{ 195 Names: fetch, 196 QueryOptions: structs.QueryOptions{ 197 Region: c.Region(), 198 AuthToken: secretID, 199 AllowStale: true, 200 }, 201 } 202 var resp structs.ACLPolicySetResponse 203 if err := c.RPC("ACL.GetPolicies", &req, &resp); err != nil { 204 // If we encounter an error but have cached policies, mask the error and extend the cache 205 if len(missing) == 0 { 206 c.logger.Warn("failed to resolve policies, using expired cached value", "error", err) 207 out = append(out, expired...) 208 return out, nil 209 } 210 return nil, err 211 } 212 213 // Handle each output 214 for _, policy := range resp.Policies { 215 c.policyCache.Add(policy.Name, &cachedACLValue{ 216 Policy: policy, 217 CacheTime: time.Now(), 218 }) 219 out = append(out, policy) 220 } 221 222 // Return the valid policies 223 return out, nil 224 }