agent/consul: implement Intention.Test endpoint

This commit is contained in:
Mitchell Hashimoto 2018-05-10 22:35:47 -07:00
parent bd5e569dc7
commit 526cfc34bd
No known key found for this signature in database
GPG key ID: 744E147AA52F5B0A
3 changed files with 372 additions and 0 deletions

View file

@ -7,6 +7,7 @@ import (
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-memdb"
@ -252,3 +253,92 @@ func (s *Intention) Match(
},
)
}
// Test tests a source/destination and returns whether it would be allowed
// or denied based on the current ACL configuration.
func (s *Intention) Test(
args *structs.IntentionQueryRequest,
reply *structs.IntentionQueryTestResponse) error {
// Get the test args, and defensively guard against nil
query := args.Test
if query == nil {
return errors.New("Test must be specified on args")
}
// Build the URI
var uri connect.CertURI
switch query.SourceType {
case structs.IntentionSourceConsul:
uri = &connect.SpiffeIDService{
Namespace: query.SourceNS,
Service: query.SourceName,
}
default:
return fmt.Errorf("unsupported SourceType: %q", query.SourceType)
}
// Get the ACL token for the request for the checks below.
rule, err := s.srv.resolveToken(args.Token)
if err != nil {
return err
}
// Perform the ACL check
if prefix, ok := query.GetACLPrefix(); ok {
if rule != nil && !rule.ServiceRead(prefix) {
s.srv.logger.Printf("[WARN] consul.intention: test on intention '%s' denied due to ACLs", prefix)
return acl.ErrPermissionDenied
}
}
// Get the matches for this destination
state := s.srv.fsm.State()
_, matches, err := state.IntentionMatch(nil, &structs.IntentionQueryMatch{
Type: structs.IntentionMatchDestination,
Entries: []structs.IntentionMatchEntry{
structs.IntentionMatchEntry{
Namespace: query.DestinationNS,
Name: query.DestinationName,
},
},
})
if err != nil {
return err
}
if len(matches) != 1 {
// This should never happen since the documented behavior of the
// Match call is that it'll always return exactly the number of results
// as entries passed in. But we guard against misbehavior.
return errors.New("internal error loading matches")
}
// Test the authorization for each match
for _, ixn := range matches[0] {
if auth, ok := uri.Authorize(ixn); ok {
reply.Allowed = auth
return nil
}
}
// No match, we need to determine the default behavior. We do this by
// specifying the anonymous token token, which will get that behavior.
// The default behavior if ACLs are disabled is to allow connections
// to mimic the behavior of Consul itself: everything is allowed if
// ACLs are disabled.
//
// NOTE(mitchellh): This is the same behavior as the agent authorize
// endpoint. If this behavior is incorrect, we should also change it there
// which is much more important.
rule, err = s.srv.resolveToken("")
if err != nil {
return err
}
reply.Allowed = true
if rule != nil {
reply.Allowed = rule.IntentionDefaultAllow()
}
return nil
}

View file

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test basic creation
@ -1007,3 +1008,256 @@ service "bar" {
assert.Equal(expected, actual)
}
}
// Test the Test method defaults to allow with no ACL set.
func TestIntentionTest_defaultNoACL(t *testing.T) {
t.Parallel()
require := require.New(t)
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Test
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "foo",
SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
var resp structs.IntentionQueryTestResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp))
require.True(resp.Allowed)
}
// Test the Test method defaults to deny with whitelist ACLs.
func TestIntentionTest_defaultACLDeny(t *testing.T) {
t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Test
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "foo",
SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = "root"
var resp structs.IntentionQueryTestResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp))
require.False(resp.Allowed)
}
// Test the Test method defaults to deny with blacklist ACLs.
func TestIntentionTest_defaultACLAllow(t *testing.T) {
t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "allow"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Test
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "foo",
SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = "root"
var resp structs.IntentionQueryTestResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp))
require.True(resp.Allowed)
}
// Test the Test method requires service:read permission.
func TestIntentionTest_aclDeny(t *testing.T) {
t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Create an ACL with service read permissions. This will grant permission.
var token string
{
var rules = `
service "bar" {
policy = "read"
}`
req := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: rules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
}
// Test
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "foo",
SourceName: "qux",
DestinationNS: "foo",
DestinationName: "baz",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = token
var resp structs.IntentionQueryTestResponse
err := msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp)
require.True(acl.IsErrPermissionDenied(err))
}
// Test the Test method returns allow/deny properly.
func TestIntentionTest_match(t *testing.T) {
t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Create an ACL with service read permissions. This will grant permission.
var token string
{
var rules = `
service "bar" {
policy = "read"
}`
req := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: rules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
}
// Create some intentions
{
insert := [][]string{
{"foo", "*", "foo", "*"},
{"foo", "*", "foo", "bar"},
{"bar", "*", "foo", "bar"}, // duplicate destination different source
}
for _, v := range insert {
ixn := structs.IntentionRequest{
Datacenter: "dc1",
Op: structs.IntentionOpCreate,
Intention: &structs.Intention{
SourceNS: v[0],
SourceName: v[1],
DestinationNS: v[2],
DestinationName: v[3],
Action: structs.IntentionActionAllow,
},
}
ixn.WriteRequest.Token = "root"
// Create
var reply string
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
}
}
// Test
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "foo",
SourceName: "qux",
DestinationNS: "foo",
DestinationName: "bar",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = token
var resp structs.IntentionQueryTestResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp))
require.True(resp.Allowed)
// Test no match for sanity
{
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Test: &structs.IntentionQueryTest{
SourceNS: "baz",
SourceName: "qux",
DestinationNS: "foo",
DestinationName: "bar",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = token
var resp structs.IntentionQueryTestResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Test", req, &resp))
require.False(resp.Allowed)
}
}

View file

@ -261,6 +261,10 @@ type IntentionQueryRequest struct {
// resolving wildcards.
Match *IntentionQueryMatch
// Test is non-nil if we're performing a test query. A test will
// return allowed/deny based on an exact match.
Test *IntentionQueryTest
// Options for queries
QueryOptions
}
@ -313,6 +317,30 @@ type IntentionMatchEntry struct {
Name string
}
// IntentionQueryTest are the parameters for performing a test request.
type IntentionQueryTest struct {
// SourceNS, SourceName, DestinationNS, and DestinationName are the
// source and namespace, respectively, for the test. These must be
// exact values.
SourceNS, SourceName string
DestinationNS, DestinationName string
// SourceType is the type of the value for the source.
SourceType IntentionSourceType
}
// GetACLPrefix returns the prefix to look up the ACL policy for this
// request, and a boolean noting whether the prefix is valid to check
// or not. You must check the ok value before using the prefix.
func (q *IntentionQueryTest) GetACLPrefix() (string, bool) {
return q.DestinationName, q.DestinationName != ""
}
// IntentionQueryTestResponse is the response for a test request.
type IntentionQueryTestResponse struct {
Allowed bool
}
// IntentionPrecedenceSorter takes a list of intentions and sorts them
// based on the match precedence rules for intentions. The intentions
// closer to the head of the list have higher precedence. i.e. index 0 has