func TestACLEndpoint_GetPolicy(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) defer codec.Close() testutil.WaitForLeader(t, s1.RPC, "dc1") arg := structs.ACLRequest{ Datacenter: "dc1", Op: structs.ACLSet, ACL: structs.ACL{ Name: "User token", Type: structs.ACLTypeClient, }, WriteRequest: structs.WriteRequest{Token: "root"}, } var out string if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &arg, &out); err != nil { t.Fatalf("err: %v", err) } getR := structs.ACLPolicyRequest{ Datacenter: "dc1", ACL: out, } var acls structs.ACLPolicy if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", &getR, &acls); err != nil { t.Fatalf("err: %v", err) } if acls.Policy == nil { t.Fatalf("Bad: %v", acls) } if acls.TTL != 30*time.Second { t.Fatalf("bad: %v", acls) } // Do a conditional lookup with etag getR.ETag = acls.ETag var out2 structs.ACLPolicy if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", &getR, &out2); err != nil { t.Fatalf("err: %v", err) } if out2.Policy != nil { t.Fatalf("Bad: %v", out2) } if out2.TTL != 30*time.Second { t.Fatalf("bad: %v", out2) } }
// lookupACL is used when we are non-authoritative, and need // to resolve an ACL func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { // Check the cache for the ACL var cached *aclCacheEntry raw, ok := s.aclCache.Get(id) if ok { cached = raw.(*aclCacheEntry) } // Check for live cache if cached != nil && time.Now().Before(cached.Expires) { metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1) return cached.ACL, nil } else { metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1) } // Attempt to refresh the policy args := structs.ACLPolicyRequest{ Datacenter: authDC, ACL: id, } if cached != nil { args.ETag = cached.ETag } var out structs.ACLPolicy err := s.RPC("ACL.GetPolicy", &args, &out) // Handle the happy path if err == nil { return s.useACLPolicy(id, authDC, cached, &out) } // Check for not-found if strings.Contains(err.Error(), aclNotFound) { return nil, errors.New(aclNotFound) } else { s.logger.Printf("[ERR] consul.acl: Failed to get policy for '%s': %v", id, err) } // Unable to refresh, apply the down policy switch s.config.ACLDownPolicy { case "allow": return acl.AllowAll(), nil case "extend-cache": if cached != nil { return cached.ACL, nil } fallthrough default: return acl.DenyAll(), nil } }
// lookupACL attempts to locate the compiled policy associated with the given // token. The agent may be used to perform RPC calls to the servers to fetch // policies that aren't in the cache. func (m *aclManager) lookupACL(agent *Agent, id string) (acl.ACL, error) { // Handle some special cases for the ID. if len(id) == 0 { id = anonymousToken } else if acl.RootACL(id) != nil { return nil, errors.New(rootDenied) } else if m.master != nil && id == agent.config.ACLAgentMasterToken { return m.master, nil } // Try the cache first. var cached *aclCacheEntry if raw, ok := m.acls.Get(id); ok { cached = raw.(*aclCacheEntry) } if cached != nil && time.Now().Before(cached.Expires) { metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1) return cached.ACL, nil } else { metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1) } // At this point we might have a stale cached ACL, or none at all, so // try to contact the servers. args := structs.ACLPolicyRequest{ Datacenter: agent.config.Datacenter, ACL: id, } if cached != nil { args.ETag = cached.ETag } var reply structs.ACLPolicy err := agent.RPC(agent.getEndpoint("ACL")+".GetPolicy", &args, &reply) if err != nil { if strings.Contains(err.Error(), aclDisabled) { agent.logger.Printf("[DEBUG] agent: ACLs disabled on servers, will check again after %s", agent.config.ACLDisabledTTL) m.disabledLock.Lock() m.disabled = time.Now().Add(agent.config.ACLDisabledTTL) m.disabledLock.Unlock() return nil, nil } else if strings.Contains(err.Error(), aclNotFound) { return nil, errors.New(aclNotFound) } else { agent.logger.Printf("[DEBUG] agent: Failed to get policy for ACL from servers: %v", err) if m.down != nil { return m.down, nil } else if cached != nil { return cached.ACL, nil } else { return acl.DenyAll(), nil } } } // Use the old cached compiled ACL if we can, otherwise compile it and // resolve any parents. var compiled acl.ACL if cached != nil && cached.ETag == reply.ETag { compiled = cached.ACL } else { parent := acl.RootACL(reply.Parent) if parent == nil { parent, err = m.lookupACL(agent, reply.Parent) if err != nil { return nil, err } } acl, err := acl.New(parent, reply.Policy) if err != nil { return nil, err } compiled = acl } // Update the cache. cached = &aclCacheEntry{ ACL: compiled, ETag: reply.ETag, } if reply.TTL > 0 { cached.Expires = time.Now().Add(reply.TTL) } m.acls.Add(id, cached) return compiled, nil }
// lookupACL is used when we are non-authoritative, and need to resolve an ACL. func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) { // Check the cache for the ACL. var cached *aclCacheEntry raw, ok := c.acls.Get(id) if ok { cached = raw.(*aclCacheEntry) } // Check for live cache. if cached != nil && time.Now().Before(cached.Expires) { metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1) return cached.ACL, nil } else { metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1) } // Attempt to refresh the policy from the ACL datacenter via an RPC. args := structs.ACLPolicyRequest{ Datacenter: authDC, ACL: id, } if cached != nil { args.ETag = cached.ETag } var reply structs.ACLPolicy err := c.rpc("ACL.GetPolicy", &args, &reply) if err == nil { return c.useACLPolicy(id, authDC, cached, &reply) } // Check for not-found, which will cause us to bail immediately. For any // other error we report it in the logs but can continue. if strings.Contains(err.Error(), aclNotFound) { return nil, errors.New(aclNotFound) } else { c.logger.Printf("[ERR] consul.acl: Failed to get policy from ACL datacenter: %v", err) } // TODO (slackpad) - We could do a similar thing *within* the ACL // datacenter if the leader isn't available. We have a local state // store of the ACLs, so by populating the local member in this cache, // it would fall back to the state store if there was a leader loss and // the extend-cache policy was true. This feels subtle to explain and // configure, and leader blips should be paved over by cache already, so // we won't do this for now but should consider for the future. This is // a lot different than the replication story where you might be cut off // from the ACL datacenter for an extended period of time and need to // carry on operating with the full set of ACLs as they were known // before the partition. // At this point we might have an expired cache entry and we know that // there was a problem getting the ACL from the ACL datacenter. If a // local ACL fault function is registered to query replicated ACL data, // and the user's policy allows it, we will try locally before we give // up. if c.local != nil && c.config.ACLDownPolicy == "extend-cache" { parent, rules, err := c.local(id) if err != nil { // We don't make an exception here for ACLs that aren't // found locally. It seems more robust to use an expired // cached entry (if we have one) rather than ignore it // for the case that replication was a bit behind and // didn't have the ACL yet. c.logger.Printf("[DEBUG] consul.acl: Failed to get policy from replicated ACLs: %v", err) goto ACL_DOWN } policy, err := acl.Parse(rules) if err != nil { c.logger.Printf("[DEBUG] consul.acl: Failed to parse policy for replicated ACL: %v", err) goto ACL_DOWN } policy.ID = acl.RuleID(rules) // Fake up an ACL datacenter reply and inject it into the cache. // Note we use the local TTL here, so this'll be used for that // amount of time even once the ACL datacenter becomes available. metrics.IncrCounter([]string{"consul", "acl", "replication_hit"}, 1) reply.ETag = makeACLETag(parent, policy) reply.TTL = c.config.ACLTTL reply.Parent = parent reply.Policy = policy return c.useACLPolicy(id, authDC, cached, &reply) } ACL_DOWN: // Unable to refresh, apply the down policy. switch c.config.ACLDownPolicy { case "allow": return acl.AllowAll(), nil case "extend-cache": if cached != nil { return cached.ACL, nil } fallthrough default: return acl.DenyAll(), nil } }