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/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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue