Adds ACL management support to the agent.
This commit is contained in:
parent
f144ce818e
commit
927aa3789a
|
@ -0,0 +1,251 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
// There's enough behavior difference with client-side ACLs that we've
|
||||
// intentionally kept this code separate from the server-side ACL code in
|
||||
// consul/acl.go. We may refactor some of the caching logic in the future,
|
||||
// but for now we are developing this separately to see how things shake out.
|
||||
|
||||
// These must be kept in sync with the constants in consul/acl.go.
|
||||
const (
|
||||
// aclNotFound indicates there is no matching ACL.
|
||||
aclNotFound = "ACL not found"
|
||||
|
||||
// rootDenied is returned when attempting to resolve a root ACL.
|
||||
rootDenied = "Cannot resolve root ACL"
|
||||
|
||||
// permissionDenied is returned when an ACL based rejection happens.
|
||||
permissionDenied = "Permission denied"
|
||||
|
||||
// aclDisabled is returned when ACL changes are not permitted since they
|
||||
// are disabled.
|
||||
aclDisabled = "ACL support disabled"
|
||||
|
||||
// anonymousToken is the token ID we re-write to if there is no token ID
|
||||
// provided.
|
||||
anonymousToken = "anonymous"
|
||||
|
||||
// Maximum number of cached ACL entries.
|
||||
aclCacheSize = 10 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
permissionDeniedErr = errors.New(permissionDenied)
|
||||
)
|
||||
|
||||
// aclCacheEntry is used to cache ACL tokens.
|
||||
type aclCacheEntry struct {
|
||||
// ACL is the cached ACL.
|
||||
ACL acl.ACL
|
||||
|
||||
// Expires is set based on the TTL for the ACL.
|
||||
Expires time.Time
|
||||
|
||||
// ETag is used as an optimization when fetching ACLs from servers to
|
||||
// avoid transmitting data back when the agent has a good copy, which is
|
||||
// usually the case when refreshing a TTL.
|
||||
ETag string
|
||||
}
|
||||
|
||||
// aclManager is used by the agent to keep track of state related to ACLs,
|
||||
// including caching tokens from the servers. This has some internal state that
|
||||
// we don't want to dump into the agent itself.
|
||||
type aclManager struct {
|
||||
// acls is a cache mapping ACL tokens to compiled policies.
|
||||
acls *lru.TwoQueueCache
|
||||
|
||||
// master is the ACL to use when the agent master token is supplied.
|
||||
// This may be nil if that option isn't set in the agent config.
|
||||
master acl.ACL
|
||||
|
||||
// down is the ACL to use when the servers are down. This may be nil
|
||||
// which means to try and use the cached policy if there is one (or
|
||||
// deny if there isn't a policy in the cache).
|
||||
down acl.ACL
|
||||
|
||||
// disabled is used to keep track of feedback from the servers that ACLs
|
||||
// are disabled. If the manager discovers that ACLs are disabled, this
|
||||
// will be set to the next time we should check to see if they have been
|
||||
// enabled. This helps cut useless traffic, but allows us to turn on ACL
|
||||
// support at the servers without having to restart the whole cluster.
|
||||
disabled time.Time
|
||||
disabledLock sync.RWMutex
|
||||
}
|
||||
|
||||
// newACLManager returns an ACL manager based on the given config.
|
||||
func newACLManager(config *Config) (*aclManager, error) {
|
||||
// Set up the cache from ID to ACL (we don't cache policies like the
|
||||
// servers; only one level).
|
||||
acls, err := lru.New2Q(aclCacheSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If an agent master token is configured, build a policy and ACL for
|
||||
// it, otherwise leave it nil.
|
||||
var master acl.ACL
|
||||
if len(config.ACLAgentMasterToken) > 0 {
|
||||
policy := &acl.Policy{
|
||||
Agents: []*acl.AgentPolicy{
|
||||
&acl.AgentPolicy{
|
||||
Node: config.NodeName,
|
||||
Policy: acl.PolicyWrite,
|
||||
},
|
||||
},
|
||||
}
|
||||
acl, err := acl.New(acl.DenyAll(), policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
master = acl
|
||||
}
|
||||
|
||||
var down acl.ACL
|
||||
switch config.ACLDownPolicy {
|
||||
case "allow":
|
||||
down = acl.AllowAll()
|
||||
case "deny":
|
||||
down = acl.DenyAll()
|
||||
case "extend-cache":
|
||||
// Leave the down policy as nil to signal this.
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL down policy %q", config.ACLDownPolicy)
|
||||
}
|
||||
|
||||
// Give back a manager.
|
||||
return &aclManager{
|
||||
acls: acls,
|
||||
master: master,
|
||||
down: down,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isDisabled returns true if the manager has discovered that ACLs are disabled
|
||||
// on the servers.
|
||||
func (m *aclManager) isDisabled() bool {
|
||||
m.disabledLock.RLock()
|
||||
defer m.disabledLock.RUnlock()
|
||||
return time.Now().Before(m.disabled)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// resolveToken is the primary interface used by ACL-checkers in the agent
|
||||
// endpoints, which is the one place where we do some ACL enforcement on
|
||||
// clients. Some of the enforcement is normative (e.g. self and monitor)
|
||||
// and some is informative (e.g. catalog and health).
|
||||
func (a *Agent) resolveToken(id string) (acl.ACL, error) {
|
||||
// Disable ACLs if version 8 enforcement isn't enabled.
|
||||
if !(*a.config.ACLEnforceVersion8) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Bail if the ACL manager is disabled. This happens if it gets feedback
|
||||
// from the servers that ACLs are disabled.
|
||||
if a.acls.isDisabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// This will look in the cache and fetch from the servers if necessary.
|
||||
return a.acls.lookupACL(a, id)
|
||||
}
|
|
@ -13,8 +13,8 @@ type aclCreateResponse struct {
|
|||
ID string
|
||||
}
|
||||
|
||||
// aclDisabled handles if ACL datacenter is not configured
|
||||
func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// ACLDisabled handles if ACL datacenter is not configured
|
||||
func ACLDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte("ACL support disabled"))
|
||||
return nil, nil
|
||||
|
|
|
@ -0,0 +1,474 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
rawacl "github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/testutil"
|
||||
)
|
||||
|
||||
func TestACL_Bad_Config(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLDownPolicy = "nope"
|
||||
|
||||
var err error
|
||||
config.DataDir, err = ioutil.TempDir("", "agent")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(config.DataDir)
|
||||
|
||||
_, err = Create(config, nil, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid ACL down policy") {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type MockServer struct {
|
||||
getPolicyFn func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error
|
||||
}
|
||||
|
||||
func (m *MockServer) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
if m.getPolicyFn != nil {
|
||||
return m.getPolicyFn(args, reply)
|
||||
} else {
|
||||
return fmt.Errorf("should not have called GetPolicy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Version8(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLEnforceVersion8 = Bool(false)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// With version 8 enforcement off, this should not get called.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
t.Fatalf("should not have called to server")
|
||||
return nil
|
||||
}
|
||||
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
|
||||
t.Fatalf("bad: %v err: %v", token, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Disabled(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLDisabledTTL = 10 * time.Millisecond
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Fetch a token without ACLs enabled and make sure the manager sees it.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return errors.New(aclDisabled)
|
||||
}
|
||||
if agent.acls.isDisabled() {
|
||||
t.Fatalf("should not be disabled yet")
|
||||
}
|
||||
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
|
||||
t.Fatalf("bad: %v err: %v", token, err)
|
||||
}
|
||||
if !agent.acls.isDisabled() {
|
||||
t.Fatalf("should be disabled")
|
||||
}
|
||||
|
||||
// Now turn on ACLs and check right away, it should still think ACLs are
|
||||
// disabled since we don't check again right away.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return errors.New(aclNotFound)
|
||||
}
|
||||
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
|
||||
t.Fatalf("bad: %v err: %v", token, err)
|
||||
}
|
||||
if !agent.acls.isDisabled() {
|
||||
t.Fatalf("should be disabled")
|
||||
}
|
||||
|
||||
// Wait the waiting period and make sure it checks again. Do a few tries
|
||||
// to make sure we don't think it's disabled.
|
||||
time.Sleep(2 * config.ACLDisabledTTL)
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := agent.resolveToken("nope")
|
||||
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if agent.acls.isDisabled() {
|
||||
t.Fatalf("should not be disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Special_IDs(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
config.ACLAgentMasterToken = "towel"
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// An empty ID should get mapped to the anonymous token.
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
if req.ACL != "anonymous" {
|
||||
t.Fatalf("bad: %#v", *req)
|
||||
}
|
||||
return errors.New(aclNotFound)
|
||||
}
|
||||
_, err := agent.resolveToken("")
|
||||
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// A root ACL request should get rejected and not call the server.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
t.Fatalf("should not have called to server")
|
||||
return nil
|
||||
}
|
||||
_, err = agent.resolveToken("deny")
|
||||
if err == nil || !strings.Contains(err.Error(), rootDenied) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// The ACL master token should also not call the server, but should give
|
||||
// us a working agent token.
|
||||
acl, err := agent.resolveToken("towel")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should be able to read agent")
|
||||
}
|
||||
if !acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should be able to write agent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Down_Deny(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLDownPolicy = "deny"
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Resolve with ACLs down.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return fmt.Errorf("ACLs are broken")
|
||||
}
|
||||
acl, err := agent.resolveToken("nope")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Down_Allow(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLDownPolicy = "allow"
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Resolve with ACLs down.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return fmt.Errorf("ACLs are broken")
|
||||
}
|
||||
acl, err := agent.resolveToken("nope")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Down_Extend(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLDownPolicy = "extend-cache"
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Populate the cache for one of the tokens.
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
Parent: "allow",
|
||||
Policy: &rawacl.Policy{
|
||||
Agents: []*rawacl.AgentPolicy{
|
||||
&rawacl.AgentPolicy{
|
||||
Node: config.NodeName,
|
||||
Policy: "read",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
acl, err := agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Now take down ACLs and make sure a new token fails to resolve.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return fmt.Errorf("ACLs are broken")
|
||||
}
|
||||
acl, err = agent.resolveToken("nope")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Read the token from the cache while ACLs are broken, which should
|
||||
// extend.
|
||||
acl, err = agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Cache(t *testing.T) {
|
||||
config := nextConfig()
|
||||
config.ACLEnforceVersion8 = Bool(true)
|
||||
|
||||
dir, agent := makeAgent(t, config)
|
||||
defer os.RemoveAll(dir)
|
||||
defer agent.Shutdown()
|
||||
|
||||
testutil.WaitForLeader(t, agent.RPC, "dc1")
|
||||
|
||||
m := MockServer{}
|
||||
if err := agent.InjectEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Populate the cache for one of the tokens.
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
ETag: "hash1",
|
||||
Parent: "deny",
|
||||
Policy: &rawacl.Policy{
|
||||
Agents: []*rawacl.AgentPolicy{
|
||||
&rawacl.AgentPolicy{
|
||||
Node: config.NodeName,
|
||||
Policy: "read",
|
||||
},
|
||||
},
|
||||
},
|
||||
TTL: 10 * time.Millisecond,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
acl, err := agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if acl.NodeRead("nope") {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Fetch right away and make sure it uses the cache.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
t.Fatalf("should not have called to server")
|
||||
return nil
|
||||
}
|
||||
acl, err = agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if acl.NodeRead("nope") {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Wait for the TTL to expire and try again. This time the token will be
|
||||
// gone.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
return errors.New(aclNotFound)
|
||||
}
|
||||
_, err = agent.resolveToken("yep")
|
||||
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Page it back in with a new tag and different policy
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
ETag: "hash2",
|
||||
Parent: "deny",
|
||||
Policy: &rawacl.Policy{
|
||||
Agents: []*rawacl.AgentPolicy{
|
||||
&rawacl.AgentPolicy{
|
||||
Node: config.NodeName,
|
||||
Policy: "write",
|
||||
},
|
||||
},
|
||||
},
|
||||
TTL: 10 * time.Millisecond,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
acl, err = agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.NodeRead("nope") {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Wait for the TTL to expire and try again. This will match the tag
|
||||
// and not send the policy back, but we should have the old token
|
||||
// behavior.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
var didRefresh bool
|
||||
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
ETag: "hash2",
|
||||
TTL: 10 * time.Millisecond,
|
||||
}
|
||||
didRefresh = true
|
||||
return nil
|
||||
}
|
||||
acl, err = agent.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !acl.AgentWrite(config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.NodeRead("nope") {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if !didRefresh {
|
||||
t.Fatalf("should refresh")
|
||||
}
|
||||
}
|
|
@ -74,6 +74,9 @@ type Agent struct {
|
|||
server *consul.Server
|
||||
client *consul.Client
|
||||
|
||||
// acls is an object that helps manage local ACL enforcement.
|
||||
acls *aclManager
|
||||
|
||||
// state stores a local representation of the node,
|
||||
// services and checks. Used for anti-entropy.
|
||||
state localState
|
||||
|
@ -211,11 +214,17 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the ACL manager.
|
||||
acls, err := newACLManager(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agent.acls = acls
|
||||
|
||||
// Initialize the local state.
|
||||
agent.state.Init(config, agent.logger)
|
||||
|
||||
// Setup either the client or the server.
|
||||
var err error
|
||||
if config.Server {
|
||||
err = agent.setupServer()
|
||||
agent.state.SetIface(agent.server)
|
||||
|
|
|
@ -55,6 +55,7 @@ func nextConfig() *Config {
|
|||
conf.Ports.SerfWan = basePortNumber + idx + portOffsetSerfWan
|
||||
conf.Ports.Server = basePortNumber + idx + portOffsetServer
|
||||
conf.Server = true
|
||||
conf.ACLEnforceVersion8 = Bool(false)
|
||||
conf.ACLDatacenter = "dc1"
|
||||
conf.ACLMasterToken = "root"
|
||||
|
||||
|
|
|
@ -488,6 +488,11 @@ type Config struct {
|
|||
// token is not provided. If not configured the 'anonymous' token is used.
|
||||
ACLToken string `mapstructure:"acl_token" json:"-"`
|
||||
|
||||
// ACLAgentMasterToken is a special token that has full read and write
|
||||
// privileges for this agent, and can be used to call agent endpoints
|
||||
// when no servers are available.
|
||||
ACLAgentMasterToken string `mapstructure:"acl_agent_master_token" json:"-"`
|
||||
|
||||
// ACLAgentToken is the default token used to make requests for the agent
|
||||
// itself, such as for registering itself with the catalog. If not
|
||||
// configured, the 'acl_token' will be used.
|
||||
|
@ -514,9 +519,15 @@ type Config struct {
|
|||
// white-lists.
|
||||
ACLDefaultPolicy string `mapstructure:"acl_default_policy"`
|
||||
|
||||
// ACLDisabledTTL is used by clients to determine how long they will
|
||||
// wait to check again with the servers if they discover ACLs are not
|
||||
// enabled.
|
||||
ACLDisabledTTL time.Duration `mapstructure:"-"`
|
||||
|
||||
// ACLDownPolicy is used to control the ACL interaction when we cannot
|
||||
// reach the ACLDatacenter and the token is not in the cache.
|
||||
// There are two modes:
|
||||
// * allow - Allow all requests
|
||||
// * deny - Deny all requests
|
||||
// * extend-cache - Ignore the cache expiration, and allow cached
|
||||
// ACL's to be used to service requests. This
|
||||
|
@ -717,6 +728,7 @@ func DefaultConfig() *Config {
|
|||
ACLTTL: 30 * time.Second,
|
||||
ACLDownPolicy: "extend-cache",
|
||||
ACLDefaultPolicy: "allow",
|
||||
ACLDisabledTTL: 120 * time.Second,
|
||||
ACLEnforceVersion8: Bool(false),
|
||||
RetryInterval: 30 * time.Second,
|
||||
RetryIntervalWan: 30 * time.Second,
|
||||
|
@ -1483,6 +1495,9 @@ func MergeConfig(a, b *Config) *Config {
|
|||
if b.ACLToken != "" {
|
||||
result.ACLToken = b.ACLToken
|
||||
}
|
||||
if b.ACLAgentMasterToken != "" {
|
||||
result.ACLAgentMasterToken = b.ACLAgentMasterToken
|
||||
}
|
||||
if b.ACLAgentToken != "" {
|
||||
result.ACLAgentToken = b.ACLAgentToken
|
||||
}
|
||||
|
|
|
@ -643,7 +643,8 @@ func TestDecodeConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
// ACLs
|
||||
input = `{"acl_token": "1234", "acl_agent_token": "5678", "acl_datacenter": "dc2",
|
||||
input = `{"acl_token": "1111", "acl_agent_master_token": "2222",
|
||||
"acl_agent_token": "3333", "acl_datacenter": "dc2",
|
||||
"acl_ttl": "60s", "acl_down_policy": "deny",
|
||||
"acl_default_policy": "deny", "acl_master_token": "2345",
|
||||
"acl_replication_token": "8675309"}`
|
||||
|
@ -652,10 +653,13 @@ func TestDecodeConfig(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if config.ACLToken != "1234" {
|
||||
if config.ACLToken != "1111" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLAgentToken != "5678" {
|
||||
if config.ACLAgentMasterToken != "2222" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLAgentToken != "3333" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLMasterToken != "2345" {
|
||||
|
@ -1589,9 +1593,10 @@ func TestMergeConfig(t *testing.T) {
|
|||
ReconnectTimeoutWan: 36 * time.Hour,
|
||||
CheckUpdateInterval: 8 * time.Minute,
|
||||
CheckUpdateIntervalRaw: "8m",
|
||||
ACLToken: "1234",
|
||||
ACLAgentToken: "5678",
|
||||
ACLMasterToken: "2345",
|
||||
ACLToken: "1111",
|
||||
ACLAgentMasterToken: "2222",
|
||||
ACLAgentToken: "3333",
|
||||
ACLMasterToken: "4444",
|
||||
ACLDatacenter: "dc2",
|
||||
ACLTTL: 15 * time.Second,
|
||||
ACLTTLRaw: "15s",
|
||||
|
|
|
@ -241,13 +241,13 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
|||
s.handleFuncMetrics("/v1/acl/list", s.wrap(s.ACLList))
|
||||
s.handleFuncMetrics("/v1/acl/replication", s.wrap(s.ACLReplicationStatus))
|
||||
} else {
|
||||
s.handleFuncMetrics("/v1/acl/create", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/update", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/info/", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/clone/", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/list", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/replication", s.wrap(aclDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/create", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/update", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/info/", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/clone/", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/list", s.wrap(ACLDisabled))
|
||||
s.handleFuncMetrics("/v1/acl/replication", s.wrap(ACLDisabled))
|
||||
}
|
||||
s.handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf))
|
||||
s.handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance))
|
||||
|
|
|
@ -18,9 +18,6 @@ import (
|
|||
const (
|
||||
syncStaggerIntv = 3 * time.Second
|
||||
syncRetryIntv = 15 * time.Second
|
||||
|
||||
// permissionDenied is returned when an ACL based rejection happens
|
||||
permissionDenied = "Permission denied"
|
||||
)
|
||||
|
||||
// syncStatus is used to represent the difference between
|
||||
|
|
|
@ -14,29 +14,30 @@ import (
|
|||
"github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
// These must be kept in sync with the constants in command/agent/acl.go.
|
||||
const (
|
||||
// aclNotFound indicates there is no matching ACL
|
||||
// aclNotFound indicates there is no matching ACL.
|
||||
aclNotFound = "ACL not found"
|
||||
|
||||
// rootDenied is returned when attempting to resolve a root ACL
|
||||
// rootDenied is returned when attempting to resolve a root ACL.
|
||||
rootDenied = "Cannot resolve root ACL"
|
||||
|
||||
// permissionDenied is returned when an ACL based rejection happens
|
||||
// permissionDenied is returned when an ACL based rejection happens.
|
||||
permissionDenied = "Permission denied"
|
||||
|
||||
// aclDisabled is returned when ACL changes are not permitted
|
||||
// since they are disabled.
|
||||
// aclDisabled is returned when ACL changes are not permitted since they
|
||||
// are disabled.
|
||||
aclDisabled = "ACL support disabled"
|
||||
|
||||
// anonymousToken is the token ID we re-write to if there
|
||||
// is no token ID provided
|
||||
// anonymousToken is the token ID we re-write to if there is no token ID
|
||||
// provided.
|
||||
anonymousToken = "anonymous"
|
||||
|
||||
// redactedToken is shown in structures with embedded tokens when they
|
||||
// are not allowed to be displayed
|
||||
// are not allowed to be displayed.
|
||||
redactedToken = "<hidden>"
|
||||
|
||||
// Maximum number of cached ACL entries
|
||||
// Maximum number of cached ACL entries.
|
||||
aclCacheSize = 10 * 1024
|
||||
)
|
||||
|
||||
|
@ -264,6 +265,8 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str
|
|||
// Check if we can used the cached policy
|
||||
if cached != nil && cached.ETag == p.ETag {
|
||||
if p.TTL > 0 {
|
||||
// TODO (slackpad) - This seems like it's an unsafe
|
||||
// write.
|
||||
cached.Expires = time.Now().Add(p.TTL)
|
||||
}
|
||||
return cached.ACL, nil
|
||||
|
|
|
@ -377,7 +377,14 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
|
|||
all operations, and "extend-cache" allows any cached ACLs to be used, ignoring their TTL
|
||||
values. If a non-cached ACL is used, "extend-cache" acts like "deny".
|
||||
|
||||
* <a name"acl_agent_token"></a><a href="#acl_agent_token">`acl_agent_token`</a> - Used for clients
|
||||
* <a name="acl_agent_master_token"></a><a href="#acl_agent_master_token">`acl_agent_master_token`</a> -
|
||||
Used to access <a href="/docs/agent/http/agent.html">agent endpoints</a> that require agent read
|
||||
or write privileges even if Consul servers aren't present to validate any tokens. This should only
|
||||
be used by operators during outages, regular ACL tokens should normally be used by applications.
|
||||
This was added in Consul 0.7.2 and is only used when <a href="#acl_enforce_version_8">`acl_enforce_version_8`</a>
|
||||
is set to true.
|
||||
|
||||
* <a name="acl_agent_token"></a><a href="#acl_agent_token">`acl_agent_token`</a> - Used for clients
|
||||
and servers to perform internal operations to the service catalog. If this isn't specified, then
|
||||
the <a href="#acl_token">`acl_token`</a> will be used. This was added in Consul 0.7.2.
|
||||
<br><br>
|
||||
|
|
Loading…
Reference in New Issue