agent/consul: implement Intention.Test endpoint
This commit is contained in:
parent
bd5e569dc7
commit
526cfc34bd
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/go-memdb"
|
"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
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
"github.com/hashicorp/net-rpc-msgpackrpc"
|
"github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test basic creation
|
// Test basic creation
|
||||||
|
@ -1007,3 +1008,256 @@ service "bar" {
|
||||||
assert.Equal(expected, actual)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -261,6 +261,10 @@ type IntentionQueryRequest struct {
|
||||||
// resolving wildcards.
|
// resolving wildcards.
|
||||||
Match *IntentionQueryMatch
|
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
|
// Options for queries
|
||||||
QueryOptions
|
QueryOptions
|
||||||
}
|
}
|
||||||
|
@ -313,6 +317,30 @@ type IntentionMatchEntry struct {
|
||||||
Name string
|
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
|
// IntentionPrecedenceSorter takes a list of intentions and sorts them
|
||||||
// based on the match precedence rules for intentions. The intentions
|
// 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
|
// closer to the head of the list have higher precedence. i.e. index 0 has
|
||||||
|
|
Loading…
Reference in New Issue