open-consul/agent/acl_test.go
Matt Keeler 2c7844d220
Implement Client Agent Auto Config
There are a couple of things in here.

First, just like auto encrypt, any Cluster.AutoConfig RPC will implicitly use the less secure RPC mechanism.

This drastically modifies how the Consul Agent starts up and moves most of the responsibilities (other than signal handling) from the cli command and into the Agent.
2020-06-17 16:49:46 -04:00

560 lines
17 KiB
Go

package agent
import (
"fmt"
"io"
"testing"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/agent/local"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/require"
)
type authzResolver func(string) (structs.ACLIdentity, acl.Authorizer, error)
type identResolver func(string) (structs.ACLIdentity, error)
type TestACLAgent struct {
// Name is an optional name of the agent.
Name string
HCL string
// Config is the agent configuration. If Config is nil then
// TestConfig() is used. If Config.DataDir is set then it is
// the callers responsibility to clean up the data directory.
// Otherwise, a temporary data directory is created and removed
// when Shutdown() is called.
Config *config.RuntimeConfig
// LogOutput is the sink for the logs. If nil, logs are written
// to os.Stderr.
LogOutput io.Writer
// DataDir is the data directory which is used when Config.DataDir
// is not set. It is created automatically and removed when
// Shutdown() is called.
DataDir string
resolveAuthzFn authzResolver
resolveIdentFn identResolver
*Agent
}
// NewTestACLAgent does just enough so that all the code within agent/acl.go can work
// Basically it needs a local state for some of the vet* functions, a logger and a delegate.
// The key is that we are the delegate so we can control the ResolveToken responses
func NewTestACLAgent(t *testing.T, name string, hcl string, resolveAuthz authzResolver, resolveIdent identResolver) *TestACLAgent {
a := &TestACLAgent{Name: name, HCL: hcl, resolveAuthzFn: resolveAuthz, resolveIdentFn: resolveIdent}
dataDir := `data_dir = "acl-agent"`
logOutput := testutil.TestWriter(t)
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
Name: a.Name,
Level: hclog.Debug,
Output: logOutput,
})
opts := []AgentOption{
WithLogger(logger),
WithBuilderOpts(config.BuilderOpts{
HCL: []string{
TestConfigHCL(NodeID()),
a.HCL,
dataDir,
},
}),
}
agent, err := New(opts...)
require.NoError(t, err)
a.Config = agent.GetConfig()
a.Agent = agent
agent.LogOutput = logOutput
agent.logger = logger
agent.MemSink = metrics.NewInmemSink(1*time.Second, time.Minute)
a.Agent.delegate = a
a.Agent.State = local.NewState(LocalConfig(a.Config), a.Agent.logger, a.Agent.tokens)
a.Agent.State.TriggerSyncChanges = func() {}
return a
}
func (a *TestACLAgent) ACLsEnabled() bool {
// the TestACLAgent always has ACLs enabled
return true
}
func (a *TestACLAgent) UseLegacyACLs() bool {
return false
}
func (a *TestACLAgent) ResolveToken(secretID string) (acl.Authorizer, error) {
if a.resolveAuthzFn == nil {
return nil, fmt.Errorf("ResolveToken call is unexpected - no authz resolver callback set")
}
_, authz, err := a.resolveAuthzFn(secretID)
return authz, err
}
func (a *TestACLAgent) ResolveTokenToIdentityAndAuthorizer(secretID string) (structs.ACLIdentity, acl.Authorizer, error) {
if a.resolveAuthzFn == nil {
return nil, nil, fmt.Errorf("ResolveTokenToIdentityAndAuthorizer call is unexpected - no authz resolver callback set")
}
return a.resolveAuthzFn(secretID)
}
func (a *TestACLAgent) ResolveTokenToIdentity(secretID string) (structs.ACLIdentity, error) {
if a.resolveIdentFn == nil {
return nil, fmt.Errorf("ResolveTokenToIdentity call is unexpected - no ident resolver callback set")
}
return a.resolveIdentFn(secretID)
}
func (a *TestACLAgent) ResolveTokenAndDefaultMeta(secretID string, entMeta *structs.EnterpriseMeta, authzContext *acl.AuthorizerContext) (acl.Authorizer, error) {
identity, authz, err := a.ResolveTokenToIdentityAndAuthorizer(secretID)
if err != nil {
return nil, err
}
// Default the EnterpriseMeta based on the Tokens meta or actual defaults
// in the case of unknown identity
if identity != nil {
entMeta.Merge(identity.EnterpriseMetadata())
} else {
entMeta.Merge(structs.DefaultEnterpriseMeta())
}
// Use the meta to fill in the ACL authorization context
entMeta.FillAuthzContext(authzContext)
return authz, err
}
// All of these are stubs to satisfy the interface
func (a *TestACLAgent) GetLANCoordinate() (lib.CoordinateSet, error) {
return nil, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Leave() error {
return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) LANMembers() []serf.Member {
return nil
}
func (a *TestACLAgent) LANMembersAllSegments() ([]serf.Member, error) {
return nil, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) LANSegmentMembers(segment string) ([]serf.Member, error) {
return nil, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) LocalMember() serf.Member {
return serf.Member{}
}
func (a *TestACLAgent) JoinLAN(addrs []string) (n int, err error) {
return 0, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) RemoveFailedNode(node string, prune bool) error {
return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) RPC(method string, args interface{}, reply interface{}) error {
return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) SnapshotRPC(args *structs.SnapshotRequest, in io.Reader, out io.Writer, replyFn structs.SnapshotReplyFn) error {
return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Shutdown() error {
return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Stats() map[string]map[string]string {
return nil
}
func (a *TestACLAgent) ReloadConfig(config *consul.Config) error {
return fmt.Errorf("Unimplemented")
}
func TestACL_Version8EnabledByDefault(t *testing.T) {
t.Parallel()
called := false
resolveFn := func(string) (structs.ACLIdentity, acl.Authorizer, error) {
called = true
return nil, nil, acl.ErrNotFound
}
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), resolveFn, nil)
_, err := a.resolveToken("nope")
require.Error(t, err)
require.True(t, called)
}
func TestACL_AgentMasterToken(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), nil, nil)
a.loadTokens(a.config)
authz, err := a.resolveToken("towel")
require.NotNil(t, authz)
require.Nil(t, err)
require.Equal(t, acl.Allow, authz.AgentRead(a.config.NodeName, nil))
require.Equal(t, acl.Allow, authz.AgentWrite(a.config.NodeName, nil))
require.Equal(t, acl.Allow, authz.NodeRead("foobarbaz", nil))
require.Equal(t, acl.Deny, authz.NodeWrite("foobarbaz", nil))
}
func TestACL_RootAuthorizersDenied(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), nil, nil)
authz, err := a.resolveToken("deny")
require.Nil(t, authz)
require.Error(t, err)
require.True(t, acl.IsErrRootDenied(err))
authz, err = a.resolveToken("allow")
require.Nil(t, authz)
require.Error(t, err)
require.True(t, acl.IsErrRootDenied(err))
authz, err = a.resolveToken("manage")
require.Nil(t, authz)
require.Error(t, err)
require.True(t, acl.IsErrRootDenied(err))
}
func authzFromPolicy(policy *acl.Policy, cfg *acl.Config) (acl.Authorizer, error) {
return acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, cfg)
}
type testToken struct {
token structs.ACLToken
// yes the rules can exist on the token itself but that is legacy behavior
// that I would prefer these tests not rely on
rules string
}
var (
nodeROSecret = "7e80d017-bccc-492f-8dec-65f03aeaebf3"
nodeRWSecret = "e3586ee5-02a2-4bf4-9ec3-9c4be7606e8c"
serviceROSecret = "3d2c8552-df3b-4da7-9890-36885cbf56ac"
serviceRWSecret = "4a1017a2-f788-4be3-93f2-90566f1340bb"
otherRWSecret = "a38e8016-91b6-4876-b3e7-a307abbb2002"
testTokens = map[string]testToken{
nodeROSecret: {
token: structs.ACLToken{
AccessorID: "9df2d1a4-2d07-414e-8ead-6053f56ed2eb",
SecretID: nodeROSecret,
},
rules: `node_prefix "Node" { policy = "read" }`,
},
nodeRWSecret: {
token: structs.ACLToken{
AccessorID: "efb6b7d5-d343-47c1-b4cb-aa6b94d2f490",
SecretID: nodeRWSecret,
},
rules: `node_prefix "Node" { policy = "write" }`,
},
serviceROSecret: {
token: structs.ACLToken{
AccessorID: "0da53edb-36e5-4603-9c31-79965bad45f5",
SecretID: serviceROSecret,
},
rules: `service_prefix "service" { policy = "read" }`,
},
serviceRWSecret: {
token: structs.ACLToken{
AccessorID: "52504258-137a-41e6-9326-01f40e80872e",
SecretID: serviceRWSecret,
},
rules: `service_prefix "service" { policy = "write" }`,
},
otherRWSecret: {
token: structs.ACLToken{
AccessorID: "5e032c5b-c39e-4552-b5ad-8a9365b099c4",
SecretID: otherRWSecret,
},
rules: `service_prefix "other" { policy = "write" }`,
},
}
)
func catalogPolicy(token string) (structs.ACLIdentity, acl.Authorizer, error) {
tok, ok := testTokens[token]
if !ok {
return nil, nil, acl.ErrNotFound
}
policy, err := acl.NewPolicyFromSource("", 0, tok.rules, acl.SyntaxCurrent, nil, nil)
if err != nil {
return nil, nil, err
}
authz, err := authzFromPolicy(policy, nil)
return &tok.token, authz, err
}
func catalogIdent(token string) (structs.ACLIdentity, error) {
tok, ok := testTokens[token]
if !ok {
return nil, acl.ErrNotFound
}
return &tok.token, nil
}
func TestACL_vetServiceRegister(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
// Register a new service, with permission.
err := a.vetServiceRegister(serviceRWSecret, &structs.NodeService{
ID: "my-service",
Service: "service",
})
require.NoError(t, err)
// Register a new service without write privs.
err = a.vetServiceRegister(serviceROSecret, &structs.NodeService{
ID: "my-service",
Service: "service",
})
require.True(t, acl.IsErrPermissionDenied(err))
// Try to register over a service without write privs to the existing
// service.
a.State.AddService(&structs.NodeService{
ID: "my-service",
Service: "other",
}, "")
err = a.vetServiceRegister(serviceRWSecret, &structs.NodeService{
ID: "my-service",
Service: "service",
})
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_vetServiceUpdate(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
// Update a service that doesn't exist.
err := a.vetServiceUpdate(serviceRWSecret, structs.NewServiceID("my-service", nil))
require.Error(t, err)
require.Contains(t, err.Error(), "Unknown service")
// Update with write privs.
a.State.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
err = a.vetServiceUpdate(serviceRWSecret, structs.NewServiceID("my-service", nil))
require.NoError(t, err)
// Update without write privs.
err = a.vetServiceUpdate(serviceROSecret, structs.NewServiceID("my-service", nil))
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_vetCheckRegister(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
// Register a new service check with write privs.
err := a.vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
require.NoError(t, err)
// Register a new service check without write privs.
err = a.vetCheckRegister(serviceROSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
// Register a new node check with write privs.
err = a.vetCheckRegister(nodeRWSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
})
require.NoError(t, err)
// Register a new node check without write privs.
err = a.vetCheckRegister(nodeROSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
})
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
// Try to register over a service check without write privs to the
// existing service.
a.State.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
a.State.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "other",
}, "")
err = a.vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
// Try to register over a node check without write privs to the node.
a.State.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
}, "")
err = a.vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
ServiceID: "my-service",
ServiceName: "service",
})
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_vetCheckUpdate(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
// Update a check that doesn't exist.
err := a.vetCheckUpdate(nodeRWSecret, structs.NewCheckID("my-check", nil))
require.Error(t, err)
require.Contains(t, err.Error(), "Unknown check")
// Update service check with write privs.
a.State.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
a.State.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-service-check"),
ServiceID: "my-service",
ServiceName: "service",
}, "")
err = a.vetCheckUpdate(serviceRWSecret, structs.NewCheckID("my-service-check", nil))
require.NoError(t, err)
// Update service check without write privs.
err = a.vetCheckUpdate(serviceROSecret, structs.NewCheckID("my-service-check", nil))
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err), "not permission denied: %s", err.Error())
// Update node check with write privs.
a.State.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
}, "")
err = a.vetCheckUpdate(nodeRWSecret, structs.NewCheckID("my-node-check", nil))
require.NoError(t, err)
// Update without write privs.
err = a.vetCheckUpdate(nodeROSecret, structs.NewCheckID("my-node-check", nil))
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_filterMembers(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
var members []serf.Member
require.NoError(t, a.filterMembers(nodeROSecret, &members))
require.Len(t, members, 0)
members = []serf.Member{
{Name: "Node 1"},
{Name: "Nope"},
{Name: "Node 2"},
}
require.NoError(t, a.filterMembers(nodeROSecret, &members))
require.Len(t, members, 2)
require.Equal(t, members[0].Name, "Node 1")
require.Equal(t, members[1].Name, "Node 2")
}
func TestACL_filterServices(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
services := make(map[structs.ServiceID]*structs.NodeService)
require.NoError(t, a.filterServices(nodeROSecret, &services))
services[structs.NewServiceID("my-service", nil)] = &structs.NodeService{ID: "my-service", Service: "service"}
services[structs.NewServiceID("my-other", nil)] = &structs.NodeService{ID: "my-other", Service: "other"}
require.NoError(t, a.filterServices(serviceROSecret, &services))
require.Contains(t, services, structs.NewServiceID("my-service", nil))
require.NotContains(t, services, structs.NewServiceID("my-other", nil))
}
func TestACL_filterChecks(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)
checks := make(map[structs.CheckID]*structs.HealthCheck)
require.NoError(t, a.filterChecks(nodeROSecret, &checks))
checks[structs.NewCheckID("my-node", nil)] = &structs.HealthCheck{}
checks[structs.NewCheckID("my-service", nil)] = &structs.HealthCheck{ServiceName: "service"}
checks[structs.NewCheckID("my-other", nil)] = &structs.HealthCheck{ServiceName: "other"}
require.NoError(t, a.filterChecks(serviceROSecret, &checks))
_, ok := checks[structs.NewCheckID("my-node", nil)]
require.False(t, ok)
_, ok = checks[structs.NewCheckID("my-service", nil)]
require.True(t, ok)
_, ok = checks[structs.NewCheckID("my-other", nil)]
require.False(t, ok)
checks[structs.NewCheckID("my-node", nil)] = &structs.HealthCheck{}
checks[structs.NewCheckID("my-service", nil)] = &structs.HealthCheck{ServiceName: "service"}
checks[structs.NewCheckID("my-other", nil)] = &structs.HealthCheck{ServiceName: "other"}
require.NoError(t, a.filterChecks(nodeROSecret, &checks))
_, ok = checks[structs.NewCheckID("my-node", nil)]
require.True(t, ok)
_, ok = checks[structs.NewCheckID("my-service", nil)]
require.False(t, ok)
_, ok = checks[structs.NewCheckID("my-other", nil)]
require.False(t, ok)
}
func TestACL_ResolveIdentity(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), nil, catalogIdent)
// this test is meant to ensure we are calling the correct function
// which is ResolveTokenToIdentity on the Agent delegate. Our
// nil authz resolver will cause it to emit an error if used
ident, err := a.resolveIdentityFromToken(nodeROSecret)
require.NoError(t, err)
require.NotNil(t, ident)
// just double checkingto ensure if we had used the wrong function
// that an error would be produced
_, err = a.resolveToken(nodeROSecret)
require.Error(t, err)
}