You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/agent/acl_test.go

504 lines
15 KiB

package agent
import (
"fmt"
"io"
"log"
"os"
"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/logger"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/require"
)
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
// LogWriter is used for streaming logs.
LogWriter *logger.LogWriter
// 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
resolveTokenFn func(string) (acl.Authorizer, error)
*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(name string, hcl string, resolveFn func(string) (acl.Authorizer, error)) *TestACLAgent {
a := &TestACLAgent{Name: name, HCL: hcl, resolveTokenFn: resolveFn}
hclDataDir := `data_dir = "acl-agent"`
a.Config = TestConfig(
config.Source{Name: a.Name, Format: "hcl", Data: a.HCL},
config.Source{Name: a.Name + ".data_dir", Format: "hcl", Data: hclDataDir},
)
agent, err := New(a.Config, nil)
if err != nil {
panic(fmt.Sprintf("Error creating agent: %v", err))
}
a.Agent = agent
logOutput := a.LogOutput
if logOutput == nil {
logOutput = os.Stderr
}
agent.LogOutput = logOutput
agent.LogWriter = a.LogWriter
agent.logger = log.New(logOutput, a.Name+" - ", log.LstdFlags|log.Lmicroseconds)
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.resolveTokenFn == nil {
panic("This agent is useless without providing a token resolution function")
}
return a.resolveTokenFn(secretID)
}
// All of these are stubs to satisfy the interface
func (a *TestACLAgent) Encrypted() bool {
return false
}
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_Version8(t *testing.T) {
t.Parallel()
t.Run("version 8 disabled", func(t *testing.T) {
resolveFn := func(string) (acl.Authorizer, error) {
require.Fail(t, "should not have called delegate.ResolveToken")
return nil, fmt.Errorf("should not have called delegate.ResolveToken")
}
a := NewTestACLAgent(t.Name(), TestACLConfig()+`
acl_enforce_version_8 = false
`, resolveFn)
token, err := a.resolveToken("nope")
require.Nil(t, token)
require.Nil(t, err)
})
t.Run("version 8 enabled", func(t *testing.T) {
called := false
resolveFn := func(string) (acl.Authorizer, error) {
called = true
return nil, acl.ErrNotFound
}
a := NewTestACLAgent(t.Name(), TestACLConfig()+`
acl_enforce_version_8 = true
`, resolveFn)
_, err := a.resolveToken("nope")
require.Error(t, err)
require.True(t, called)
})
}
func TestACL_AgentMasterToken(t *testing.T) {
t.Parallel()
resolveFn := func(string) (acl.Authorizer, error) {
require.Fail(t, "should not have called delegate.ResolveToken")
return nil, fmt.Errorf("should not have called delegate.ResolveToken")
}
a := NewTestACLAgent(t.Name(), TestACLConfig(), resolveFn)
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()
resolveFn := func(string) (acl.Authorizer, error) {
require.Fail(t, "should not have called delegate.ResolveToken")
return nil, fmt.Errorf("should not have called delegate.ResolveToken")
}
a := NewTestACLAgent(t.Name(), TestACLConfig(), resolveFn)
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) (acl.Authorizer, error) {
return acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
}
// catalogPolicy supplies some standard policies to help with testing the
// catalog-related vet and filter functions.
func catalogPolicy(token string) (acl.Authorizer, error) {
switch token {
case "node-ro":
return authzFromPolicy(&acl.Policy{
PolicyRules: acl.PolicyRules{
NodePrefixes: []*acl.NodeRule{
&acl.NodeRule{Name: "Node", Policy: "read"},
},
},
})
case "node-rw":
return authzFromPolicy(&acl.Policy{
PolicyRules: acl.PolicyRules{
NodePrefixes: []*acl.NodeRule{
&acl.NodeRule{Name: "Node", Policy: "write"},
},
},
})
case "service-ro":
return authzFromPolicy(&acl.Policy{
PolicyRules: acl.PolicyRules{
ServicePrefixes: []*acl.ServiceRule{
&acl.ServiceRule{Name: "service", Policy: "read"},
},
},
})
case "service-rw":
return authzFromPolicy(&acl.Policy{
PolicyRules: acl.PolicyRules{
ServicePrefixes: []*acl.ServiceRule{
&acl.ServiceRule{Name: "service", Policy: "write"},
},
},
})
case "other-rw":
return authzFromPolicy(&acl.Policy{
PolicyRules: acl.PolicyRules{
ServicePrefixes: []*acl.ServiceRule{
&acl.ServiceRule{Name: "other", Policy: "write"},
},
},
})
default:
return nil, fmt.Errorf("unknown token %q", token)
}
}
func TestACL_vetServiceRegister(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t.Name(), TestACLConfig(), catalogPolicy)
// Register a new service, with permission.
err := a.vetServiceRegister("service-rw", &structs.NodeService{
ID: "my-service",
Service: "service",
})
require.NoError(t, err)
// Register a new service without write privs.
err = a.vetServiceRegister("service-ro", &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("service-rw", &structs.NodeService{
ID: "my-service",
Service: "service",
})
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_vetServiceUpdate(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t.Name(), TestACLConfig(), catalogPolicy)
// Update a service that doesn't exist.
err := a.vetServiceUpdate("service-rw", "my-service")
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("service-rw", "my-service")
require.NoError(t, err)
// Update without write privs.
err = a.vetServiceUpdate("service-ro", "my-service")
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_vetCheckRegister(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t.Name(), TestACLConfig(), catalogPolicy)
// Register a new service check with write privs.
err := a.vetCheckRegister("service-rw", &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("service-ro", &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("node-rw", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
})
require.NoError(t, err)
// Register a new node check without write privs.
err = a.vetCheckRegister("node-ro", &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("service-rw", &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("service-rw", &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.Name(), TestACLConfig(), catalogPolicy)
// Update a check that doesn't exist.
err := a.vetCheckUpdate("node-rw", "my-check")
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("service-rw", "my-service-check")
require.NoError(t, err)
// Update service check without write privs.
err = a.vetCheckUpdate("service-ro", "my-service-check")
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
// Update node check with write privs.
a.State.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
}, "")
err = a.vetCheckUpdate("node-rw", "my-node-check")
require.NoError(t, err)
// Update without write privs.
err = a.vetCheckUpdate("node-ro", "my-node-check")
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestACL_filterMembers(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t.Name(), TestACLConfig(), catalogPolicy)
var members []serf.Member
require.NoError(t, a.filterMembers("node-ro", &members))
require.Len(t, members, 0)
members = []serf.Member{
serf.Member{Name: "Node 1"},
serf.Member{Name: "Nope"},
serf.Member{Name: "Node 2"},
}
require.NoError(t, a.filterMembers("node-ro", &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.Name(), TestACLConfig(), catalogPolicy)
services := make(map[string]*structs.NodeService)
require.NoError(t, a.filterServices("node-ro", &services))
services["my-service"] = &structs.NodeService{ID: "my-service", Service: "service"}
services["my-other"] = &structs.NodeService{ID: "my-other", Service: "other"}
require.NoError(t, a.filterServices("service-ro", &services))
require.Contains(t, services, "my-service")
require.NotContains(t, services, "my-other")
}
func TestACL_filterChecks(t *testing.T) {
t.Parallel()
a := NewTestACLAgent(t.Name(), TestACLConfig(), catalogPolicy)
checks := make(map[types.CheckID]*structs.HealthCheck)
require.NoError(t, a.filterChecks("node-ro", &checks))
checks["my-node"] = &structs.HealthCheck{}
checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
require.NoError(t, a.filterChecks("service-ro", &checks))
fmt.Printf("filtered: %#v", checks)
_, ok := checks["my-node"]
require.False(t, ok)
_, ok = checks["my-service"]
require.True(t, ok)
_, ok = checks["my-other"]
require.False(t, ok)
checks["my-node"] = &structs.HealthCheck{}
checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
require.NoError(t, a.filterChecks("node-ro", &checks))
_, ok = checks["my-node"]
require.True(t, ok)
_, ok = checks["my-service"]
require.False(t, ok)
_, ok = checks["my-other"]
require.False(t, ok)
}