connect: various changes to make namespaces for intentions work more like for other subsystems (#8194)

Highlights:

- add new endpoint to query for intentions by exact match

- using this endpoint from the CLI instead of the dump+filter approach

- enforcing that OSS can only read/write intentions with a SourceNS or
  DestinationNS field of "default".

- preexisting OSS intentions with now-invalid namespace fields will
  delete those intentions on initial election or for wildcard namespaces
  an attempt will be made to downgrade them to "default" unless one
  exists.

- also allow the '-namespace' CLI arg on all of the intention subcommands

- update lots of docs
This commit is contained in:
R.B. Boyer 2020-06-26 16:59:15 -05:00 committed by GitHub
parent 10d6e9c458
commit 462f0f37ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1233 additions and 390 deletions

View File

@ -3,7 +3,9 @@
package consul package consul
import ( import (
"errors"
"net" "net"
"strings"
"github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
@ -59,6 +61,18 @@ func (s *Server) validateEnterpriseRequest(entMeta *structs.EnterpriseMeta, writ
return nil return nil
} }
func (s *Server) validateEnterpriseIntentionNamespace(ns string, _ bool) error {
if ns == "" {
return nil
} else if strings.ToLower(ns) == structs.IntentionDefaultNamespace {
return nil
}
// No special handling for wildcard namespaces as they are pointless in OSS.
return errors.New("Namespaces is a Consul Enterprise feature")
}
func (_ *Server) addEnterpriseSerfTags(_ map[string]string) { func (_ *Server) addEnterpriseSerfTags(_ map[string]string) {
// do nothing // do nothing
} }

View File

@ -576,7 +576,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
require.Equal(t, autopilotConf, restoredConf) require.Equal(t, autopilotConf, restoredConf)
// Verify intentions are restored. // Verify intentions are restored.
_, ixns, err := fsm2.state.Intentions(nil) _, ixns, err := fsm2.state.Intentions(nil, structs.WildcardEnterpriseMeta())
require.NoError(t, err) require.NoError(t, err)
require.Len(t, ixns, 1) require.Len(t, ixns, 1)
require.Equal(t, ixn, ixns[0]) require.Equal(t, ixn, ixns[0])

View File

@ -76,6 +76,10 @@ func (s *Intention) prepareApplyCreate(ident structs.ACLIdentity, authz acl.Auth
args.Intention.DefaultNamespaces(entMeta) args.Intention.DefaultNamespaces(entMeta)
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return err
}
// Validate. We do not validate on delete since it is valid to only // Validate. We do not validate on delete since it is valid to only
// send an ID in that case. // send an ID in that case.
// Set the precedence // Set the precedence
@ -135,11 +139,15 @@ func (s *Intention) prepareApplyUpdate(ident structs.ACLIdentity, authz acl.Auth
args.Intention.DefaultNamespaces(entMeta) args.Intention.DefaultNamespaces(entMeta)
// Validate. We do not validate on delete since it is valid to only if err := s.validateEnterpriseIntention(args.Intention); err != nil {
// send an ID in that case. return err
}
// Set the precedence // Set the precedence
args.Intention.UpdatePrecedence() args.Intention.UpdatePrecedence()
// Validate. We do not validate on delete since it is valid to only
// send an ID in that case.
if err := args.Intention.Validate(); err != nil { if err := args.Intention.Validate(); err != nil {
return err return err
} }
@ -249,11 +257,44 @@ func (s *Intention) Get(
return err return err
} }
// Get the ACL token for the request for the checks below.
var entMeta structs.EnterpriseMeta
if _, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil); err != nil {
return err
}
if args.Exact != nil {
// // Finish defaulting the namespace fields.
if args.Exact.SourceNS == "" {
args.Exact.SourceNS = entMeta.NamespaceOrDefault()
}
if err := s.srv.validateEnterpriseIntentionNamespace(args.Exact.SourceNS, true); err != nil {
return fmt.Errorf("Invalid SourceNS %q: %v", args.Exact.SourceNS, err)
}
if args.Exact.DestinationNS == "" {
args.Exact.DestinationNS = entMeta.NamespaceOrDefault()
}
if err := s.srv.validateEnterpriseIntentionNamespace(args.Exact.DestinationNS, true); err != nil {
return fmt.Errorf("Invalid DestinationNS %q: %v", args.Exact.DestinationNS, err)
}
}
return s.srv.blockingQuery( return s.srv.blockingQuery(
&args.QueryOptions, &args.QueryOptions,
&reply.QueryMeta, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
index, ixn, err := state.IntentionGet(ws, args.IntentionID) var (
index uint64
ixn *structs.Intention
err error
)
if args.IntentionID != "" {
index, ixn, err = state.IntentionGet(ws, args.IntentionID)
} else if args.Exact != nil {
index, ixn, err = state.IntentionGetExact(ws, args.Exact)
}
if err != nil { if err != nil {
return err return err
} }
@ -296,10 +337,19 @@ func (s *Intention) List(
return err return err
} }
var authzContext acl.AuthorizerContext
if _, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
}
if err := s.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil {
return err
}
return s.srv.blockingQuery( return s.srv.blockingQuery(
&args.QueryOptions, &reply.QueryMeta, &args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
index, ixns, err := state.Intentions(ws) index, ixns, err := state.Intentions(ws, &args.EnterpriseMeta)
if err != nil { if err != nil {
return err return err
} }
@ -334,12 +384,24 @@ func (s *Intention) Match(
} }
// Get the ACL token for the request for the checks below. // Get the ACL token for the request for the checks below.
rule, err := s.srv.ResolveToken(args.Token) var entMeta structs.EnterpriseMeta
authz, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil)
if err != nil { if err != nil {
return err return err
} }
if rule != nil { // Finish defaulting the namespace fields.
for i := range args.Match.Entries {
if args.Match.Entries[i].Namespace == "" {
args.Match.Entries[i].Namespace = entMeta.NamespaceOrDefault()
}
if err := s.srv.validateEnterpriseIntentionNamespace(args.Match.Entries[i].Namespace, true); err != nil {
return fmt.Errorf("Invalid match entry namespace %q: %v",
args.Match.Entries[i].Namespace, err)
}
}
if authz != nil {
var authzContext acl.AuthorizerContext var authzContext acl.AuthorizerContext
// Go through each entry to ensure we have intention:read for the resource. // Go through each entry to ensure we have intention:read for the resource.
@ -351,7 +413,7 @@ func (s *Intention) Match(
// matching, if you have it on the dest then perform a dest type match. // matching, if you have it on the dest then perform a dest type match.
for _, entry := range args.Match.Entries { for _, entry := range args.Match.Entries {
entry.FillAuthzContext(&authzContext) entry.FillAuthzContext(&authzContext)
if prefix := entry.Name; prefix != "" && rule.IntentionRead(prefix, &authzContext) != acl.Allow { if prefix := entry.Name; prefix != "" && authz.IntentionRead(prefix, &authzContext) != acl.Allow {
accessorID := s.aclAccessorID(args.Token) accessorID := s.aclAccessorID(args.Token)
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it // todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Operation on intention prefix denied due to ACLs", "prefix", prefix, "accessorID", accessorID) s.logger.Warn("Operation on intention prefix denied due to ACLs", "prefix", prefix, "accessorID", accessorID)
@ -396,6 +458,28 @@ func (s *Intention) Check(
return errors.New("Check must be specified on args") return errors.New("Check must be specified on args")
} }
// Get the ACL token for the request for the checks below.
var entMeta structs.EnterpriseMeta
authz, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil)
if err != nil {
return err
}
// Finish defaulting the namespace fields.
if query.SourceNS == "" {
query.SourceNS = entMeta.NamespaceOrDefault()
}
if query.DestinationNS == "" {
query.DestinationNS = entMeta.NamespaceOrDefault()
}
if err := s.srv.validateEnterpriseIntentionNamespace(query.SourceNS, false); err != nil {
return fmt.Errorf("Invalid source namespace %q: %v", query.SourceNS, err)
}
if err := s.srv.validateEnterpriseIntentionNamespace(query.DestinationNS, false); err != nil {
return fmt.Errorf("Invalid destination namespace %q: %v", query.DestinationNS, err)
}
// Build the URI // Build the URI
var uri connect.CertURI var uri connect.CertURI
switch query.SourceType { switch query.SourceType {
@ -409,12 +493,6 @@ func (s *Intention) Check(
return fmt.Errorf("unsupported SourceType: %q", query.SourceType) 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. For Check we only require ServiceRead and // Perform the ACL check. For Check we only require ServiceRead and
// NOT IntentionRead because the Check API only returns pass/fail and // NOT IntentionRead because the Check API only returns pass/fail and
// returns no other information about the intentions used. We could check // returns no other information about the intentions used. We could check
@ -424,7 +502,7 @@ func (s *Intention) Check(
if prefix, ok := query.GetACLPrefix(); ok { if prefix, ok := query.GetACLPrefix(); ok {
var authzContext acl.AuthorizerContext var authzContext acl.AuthorizerContext
query.FillAuthzContext(&authzContext) query.FillAuthzContext(&authzContext)
if rule != nil && rule.ServiceRead(prefix, &authzContext) != acl.Allow { if authz != nil && authz.ServiceRead(prefix, &authzContext) != acl.Allow {
accessorID := s.aclAccessorID(args.Token) accessorID := s.aclAccessorID(args.Token)
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it // todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("test on intention denied due to ACLs", "prefix", prefix, "accessorID", accessorID) s.logger.Warn("test on intention denied due to ACLs", "prefix", prefix, "accessorID", accessorID)
@ -470,14 +548,14 @@ func (s *Intention) Check(
// NOTE(mitchellh): This is the same behavior as the agent authorize // NOTE(mitchellh): This is the same behavior as the agent authorize
// endpoint. If this behavior is incorrect, we should also change it there // endpoint. If this behavior is incorrect, we should also change it there
// which is much more important. // which is much more important.
rule, err = s.srv.ResolveToken("") authz, err = s.srv.ResolveToken("")
if err != nil { if err != nil {
return err return err
} }
reply.Allowed = true reply.Allowed = true
if rule != nil { if authz != nil {
reply.Allowed = rule.IntentionDefaultAllow(nil) == acl.Allow reply.Allowed = authz.IntentionDefaultAllow(nil) == acl.Allow
} }
return nil return nil
@ -500,3 +578,13 @@ func (s *Intention) aclAccessorID(secretID string) string {
} }
return ident.ID() return ident.ID()
} }
func (s *Intention) validateEnterpriseIntention(ixn *structs.Intention) error {
if err := s.srv.validateEnterpriseIntentionNamespace(ixn.SourceNS, true); err != nil {
return fmt.Errorf("Invalid source namespace %q: %v", ixn.SourceNS, err)
}
if err := s.srv.validateEnterpriseIntentionNamespace(ixn.DestinationNS, true); err != nil {
return fmt.Errorf("Invalid destination namespace %q: %v", ixn.DestinationNS, err)
}
return nil
}

View File

@ -7,9 +7,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/testrpc"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -17,14 +15,14 @@ import (
func TestIntentionApply_new(t *testing.T) { func TestIntentionApply_new(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -46,8 +44,8 @@ func TestIntentionApply_new(t *testing.T) {
now := time.Now() now := time.Now()
// Create // Create
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
assert.NotEmpty(reply) require.NotEmpty(reply)
// Read // Read
ixn.Intention.ID = reply ixn.Intention.ID = reply
@ -57,19 +55,19 @@ func TestIntentionApply_new(t *testing.T) {
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(resp.Intentions, 1)
actual := resp.Intentions[0] actual := resp.Intentions[0]
assert.Equal(resp.Index, actual.ModifyIndex) require.Equal(resp.Index, actual.ModifyIndex)
assert.WithinDuration(now, actual.CreatedAt, 5*time.Second) require.WithinDuration(now, actual.CreatedAt, 5*time.Second)
assert.WithinDuration(now, actual.UpdatedAt, 5*time.Second) require.WithinDuration(now, actual.UpdatedAt, 5*time.Second)
actual.CreateIndex, actual.ModifyIndex = 0, 0 actual.CreateIndex, actual.ModifyIndex = 0, 0
actual.CreatedAt = ixn.Intention.CreatedAt actual.CreatedAt = ixn.Intention.CreatedAt
actual.UpdatedAt = ixn.Intention.UpdatedAt actual.UpdatedAt = ixn.Intention.UpdatedAt
actual.Hash = ixn.Intention.Hash actual.Hash = ixn.Intention.Hash
ixn.Intention.UpdatePrecedence() ixn.Intention.UpdatePrecedence()
assert.Equal(ixn.Intention, actual) require.Equal(ixn.Intention, actual)
} }
} }
@ -77,14 +75,14 @@ func TestIntentionApply_new(t *testing.T) {
func TestIntentionApply_defaultSourceType(t *testing.T) { func TestIntentionApply_defaultSourceType(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -101,8 +99,8 @@ func TestIntentionApply_defaultSourceType(t *testing.T) {
var reply string var reply string
// Create // Create
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
assert.NotEmpty(reply) require.NotEmpty(reply)
// Read // Read
ixn.Intention.ID = reply ixn.Intention.ID = reply
@ -112,10 +110,10 @@ func TestIntentionApply_defaultSourceType(t *testing.T) {
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(resp.Intentions, 1)
actual := resp.Intentions[0] actual := resp.Intentions[0]
assert.Equal(structs.IntentionSourceConsul, actual.SourceType) require.Equal(structs.IntentionSourceConsul, actual.SourceType)
} }
} }
@ -123,14 +121,14 @@ func TestIntentionApply_defaultSourceType(t *testing.T) {
func TestIntentionApply_createWithID(t *testing.T) { func TestIntentionApply_createWithID(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -145,22 +143,22 @@ func TestIntentionApply_createWithID(t *testing.T) {
// Create // Create
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.NotNil(err) require.NotNil(err)
assert.Contains(err, "ID must be empty") require.Contains(err, "ID must be empty")
} }
// Test basic updating // Test basic updating
func TestIntentionApply_updateGood(t *testing.T) { func TestIntentionApply_updateGood(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -179,8 +177,8 @@ func TestIntentionApply_updateGood(t *testing.T) {
var reply string var reply string
// Create // Create
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
assert.NotEmpty(reply) require.NotEmpty(reply)
// Read CreatedAt // Read CreatedAt
var createdAt time.Time var createdAt time.Time
@ -191,8 +189,8 @@ func TestIntentionApply_updateGood(t *testing.T) {
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(resp.Intentions, 1)
actual := resp.Intentions[0] actual := resp.Intentions[0]
createdAt = actual.CreatedAt createdAt = actual.CreatedAt
} }
@ -204,7 +202,7 @@ func TestIntentionApply_updateGood(t *testing.T) {
ixn.Op = structs.IntentionOpUpdate ixn.Op = structs.IntentionOpUpdate
ixn.Intention.ID = reply ixn.Intention.ID = reply
ixn.Intention.SourceName = "*" ixn.Intention.SourceName = "*"
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Read // Read
ixn.Intention.ID = reply ixn.Intention.ID = reply
@ -214,18 +212,18 @@ func TestIntentionApply_updateGood(t *testing.T) {
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(resp.Intentions, 1)
actual := resp.Intentions[0] actual := resp.Intentions[0]
assert.Equal(createdAt, actual.CreatedAt) require.Equal(createdAt, actual.CreatedAt)
assert.WithinDuration(time.Now(), actual.UpdatedAt, 5*time.Second) require.WithinDuration(time.Now(), actual.UpdatedAt, 5*time.Second)
actual.CreateIndex, actual.ModifyIndex = 0, 0 actual.CreateIndex, actual.ModifyIndex = 0, 0
actual.CreatedAt = ixn.Intention.CreatedAt actual.CreatedAt = ixn.Intention.CreatedAt
actual.UpdatedAt = ixn.Intention.UpdatedAt actual.UpdatedAt = ixn.Intention.UpdatedAt
actual.Hash = ixn.Intention.Hash actual.Hash = ixn.Intention.Hash
ixn.Intention.UpdatePrecedence() ixn.Intention.UpdatePrecedence()
assert.Equal(ixn.Intention, actual) require.Equal(ixn.Intention, actual)
} }
} }
@ -233,14 +231,14 @@ func TestIntentionApply_updateGood(t *testing.T) {
func TestIntentionApply_updateNonExist(t *testing.T) { func TestIntentionApply_updateNonExist(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -255,31 +253,29 @@ func TestIntentionApply_updateNonExist(t *testing.T) {
// Create // Create
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.NotNil(err) require.NotNil(err)
assert.Contains(err, "Cannot modify non-existent intention") require.Contains(err, "Cannot modify non-existent intention")
} }
// Test basic deleting // Test basic deleting
func TestIntentionApply_deleteGood(t *testing.T) { func TestIntentionApply_deleteGood(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
Datacenter: "dc1", Datacenter: "dc1",
Op: structs.IntentionOpCreate, Op: structs.IntentionOpCreate,
Intention: &structs.Intention{ Intention: &structs.Intention{
SourceNS: "test",
SourceName: "test", SourceName: "test",
DestinationNS: "test",
DestinationName: "test", DestinationName: "test",
Action: structs.IntentionActionAllow, Action: structs.IntentionActionAllow,
}, },
@ -287,13 +283,13 @@ func TestIntentionApply_deleteGood(t *testing.T) {
var reply string var reply string
// Create // Create
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
assert.NotEmpty(reply) require.NotEmpty(reply)
// Delete // Delete
ixn.Op = structs.IntentionOpDelete ixn.Op = structs.IntentionOpDelete
ixn.Intention.ID = reply ixn.Intention.ID = reply
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Read // Read
ixn.Intention.ID = reply ixn.Intention.ID = reply
@ -304,8 +300,8 @@ func TestIntentionApply_deleteGood(t *testing.T) {
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp) err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
assert.NotNil(err) require.NotNil(err)
assert.Contains(err, ErrIntentionNotFound.Error()) require.Contains(err, ErrIntentionNotFound.Error())
} }
} }
@ -313,7 +309,7 @@ func TestIntentionApply_deleteGood(t *testing.T) {
func TestIntentionApply_aclDeny(t *testing.T) { func TestIntentionApply_aclDeny(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -325,7 +321,7 @@ func TestIntentionApply_aclDeny(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with write permissions // Create an ACL with write permissions
var token string var token string
@ -346,7 +342,7 @@ service "foo" {
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
} }
// Setup a basic record to create // Setup a basic record to create
@ -360,11 +356,11 @@ service "foo" {
// Create without a token should error since default deny // Create without a token should error since default deny
var reply string var reply string
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.True(acl.IsErrPermissionDenied(err)) require.True(acl.IsErrPermissionDenied(err))
// Now add the token and try again. // Now add the token and try again.
ixn.WriteRequest.Token = token ixn.WriteRequest.Token = token
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Read // Read
ixn.Intention.ID = reply ixn.Intention.ID = reply
@ -375,17 +371,17 @@ service "foo" {
QueryOptions: structs.QueryOptions{Token: "root"}, QueryOptions: structs.QueryOptions{Token: "root"},
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(resp.Intentions, 1)
actual := resp.Intentions[0] actual := resp.Intentions[0]
assert.Equal(resp.Index, actual.ModifyIndex) require.Equal(resp.Index, actual.ModifyIndex)
actual.CreateIndex, actual.ModifyIndex = 0, 0 actual.CreateIndex, actual.ModifyIndex = 0, 0
actual.CreatedAt = ixn.Intention.CreatedAt actual.CreatedAt = ixn.Intention.CreatedAt
actual.UpdatedAt = ixn.Intention.UpdatedAt actual.UpdatedAt = ixn.Intention.UpdatedAt
actual.Hash = ixn.Intention.Hash actual.Hash = ixn.Intention.Hash
ixn.Intention.UpdatePrecedence() ixn.Intention.UpdatePrecedence()
assert.Equal(ixn.Intention, actual) require.Equal(ixn.Intention, actual)
} }
} }
@ -707,7 +703,7 @@ func TestIntention_WildcardACLEnforcement(t *testing.T) {
func TestIntentionApply_aclDelete(t *testing.T) { func TestIntentionApply_aclDelete(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -719,7 +715,7 @@ func TestIntentionApply_aclDelete(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with write permissions // Create an ACL with write permissions
var token string var token string
@ -740,7 +736,7 @@ service "foo" {
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
} }
// Setup a basic record to create // Setup a basic record to create
@ -754,18 +750,18 @@ service "foo" {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Try to do a delete with no token; this should get rejected. // Try to do a delete with no token; this should get rejected.
ixn.Op = structs.IntentionOpDelete ixn.Op = structs.IntentionOpDelete
ixn.Intention.ID = reply ixn.Intention.ID = reply
ixn.WriteRequest.Token = "" ixn.WriteRequest.Token = ""
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.True(acl.IsErrPermissionDenied(err)) require.True(acl.IsErrPermissionDenied(err))
// Try again with the original token. This should go through. // Try again with the original token. This should go through.
ixn.WriteRequest.Token = token ixn.WriteRequest.Token = token
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Verify it is gone // Verify it is gone
{ {
@ -775,8 +771,8 @@ service "foo" {
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp) err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
assert.NotNil(err) require.NotNil(err)
assert.Contains(err.Error(), ErrIntentionNotFound.Error()) require.Contains(err.Error(), ErrIntentionNotFound.Error())
} }
} }
@ -784,7 +780,7 @@ service "foo" {
func TestIntentionApply_aclUpdate(t *testing.T) { func TestIntentionApply_aclUpdate(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -796,7 +792,7 @@ func TestIntentionApply_aclUpdate(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with write permissions // Create an ACL with write permissions
var token string var token string
@ -817,7 +813,7 @@ service "foo" {
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
} }
// Setup a basic record to create // Setup a basic record to create
@ -831,25 +827,25 @@ service "foo" {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Try to do an update without a token; this should get rejected. // Try to do an update without a token; this should get rejected.
ixn.Op = structs.IntentionOpUpdate ixn.Op = structs.IntentionOpUpdate
ixn.Intention.ID = reply ixn.Intention.ID = reply
ixn.WriteRequest.Token = "" ixn.WriteRequest.Token = ""
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.True(acl.IsErrPermissionDenied(err)) require.True(acl.IsErrPermissionDenied(err))
// Try again with the original token; this should go through. // Try again with the original token; this should go through.
ixn.WriteRequest.Token = token ixn.WriteRequest.Token = token
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
} }
// Test apply with a management token // Test apply with a management token
func TestIntentionApply_aclManagement(t *testing.T) { func TestIntentionApply_aclManagement(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -861,7 +857,7 @@ func TestIntentionApply_aclManagement(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -874,23 +870,23 @@ func TestIntentionApply_aclManagement(t *testing.T) {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
ixn.Intention.ID = reply ixn.Intention.ID = reply
// Update // Update
ixn.Op = structs.IntentionOpUpdate ixn.Op = structs.IntentionOpUpdate
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Delete // Delete
ixn.Op = structs.IntentionOpDelete ixn.Op = structs.IntentionOpDelete
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
} }
// Test update changing the name where an ACL won't allow it // Test update changing the name where an ACL won't allow it
func TestIntentionApply_aclUpdateChange(t *testing.T) { func TestIntentionApply_aclUpdateChange(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -902,7 +898,7 @@ func TestIntentionApply_aclUpdateChange(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with write permissions // Create an ACL with write permissions
var token string var token string
@ -923,7 +919,7 @@ service "foo" {
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
} }
// Setup a basic record to create // Setup a basic record to create
@ -937,7 +933,7 @@ service "foo" {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
// Try to do an update without a token; this should get rejected. // Try to do an update without a token; this should get rejected.
ixn.Op = structs.IntentionOpUpdate ixn.Op = structs.IntentionOpUpdate
@ -945,14 +941,13 @@ service "foo" {
ixn.Intention.DestinationName = "foo" ixn.Intention.DestinationName = "foo"
ixn.WriteRequest.Token = token ixn.WriteRequest.Token = token
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
assert.True(acl.IsErrPermissionDenied(err)) require.True(acl.IsErrPermissionDenied(err))
} }
// Test reading with ACLs // Test reading with ACLs
func TestIntentionGet_acl(t *testing.T) { func TestIntentionGet_acl(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -964,29 +959,15 @@ func TestIntentionGet_acl(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with service write permissions. This will grant // Create an ACL with service write permissions. This will grant
// intentions read. // intentions read on either end of an intention.
var token string token, err := upsertTestTokenWithPolicyRules(codec, "root", "dc1", `
{ service "foobar" {
var rules = ` policy = "write"
service "foo" { }`)
policy = "write" require.NoError(t, err)
}`
req := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTokenTypeClient,
Rules: rules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
}
// Setup a basic record to create // Setup a basic record to create
ixn := structs.IntentionRequest{ ixn := structs.IntentionRequest{
@ -999,11 +980,10 @@ service "foo" {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
ixn.Intention.ID = reply ixn.Intention.ID = reply
// Read without token should be error t.Run("Read by ID without token should be error", func(t *testing.T) {
{
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
@ -1011,35 +991,68 @@ service "foo" {
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp) err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
assert.True(acl.IsErrPermissionDenied(err)) require.True(t, acl.IsErrPermissionDenied(err))
assert.Len(resp.Intentions, 0) require.Len(t, resp.Intentions, 0)
} })
// Read with token should work t.Run("Read by ID with token should work", func(t *testing.T) {
{
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
IntentionID: ixn.Intention.ID, IntentionID: ixn.Intention.ID,
QueryOptions: structs.QueryOptions{Token: token}, QueryOptions: structs.QueryOptions{Token: token.SecretID},
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
assert.Len(resp.Intentions, 1) require.Len(t, resp.Intentions, 1)
} })
t.Run("Read by Exact without token should be error", func(t *testing.T) {
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Exact: &structs.IntentionQueryExact{
SourceNS: structs.IntentionDefaultNamespace,
SourceName: "api",
DestinationNS: structs.IntentionDefaultNamespace,
DestinationName: "foobar",
},
}
var resp structs.IndexedIntentions
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
require.True(t, acl.IsErrPermissionDenied(err))
require.Len(t, resp.Intentions, 0)
})
t.Run("Read by Exact with token should work", func(t *testing.T) {
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Exact: &structs.IntentionQueryExact{
SourceNS: structs.IntentionDefaultNamespace,
SourceName: "api",
DestinationNS: structs.IntentionDefaultNamespace,
DestinationName: "foobar",
},
QueryOptions: structs.QueryOptions{Token: token.SecretID},
}
var resp structs.IndexedIntentions
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
require.Len(t, resp.Intentions, 1)
})
} }
func TestIntentionList(t *testing.T) { func TestIntentionList(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Test with no intentions inserted yet // Test with no intentions inserted yet
{ {
@ -1047,9 +1060,9 @@ func TestIntentionList(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
assert.NotNil(resp.Intentions) require.NotNil(resp.Intentions)
assert.Len(resp.Intentions, 0) require.Len(resp.Intentions, 0)
} }
} }
@ -1063,7 +1076,7 @@ func TestIntentionList_acl(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
waitForNewACLs(t, s1) waitForNewACLs(t, s1)
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service_prefix "foo" { policy = "write" }`) token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service_prefix "foo" { policy = "write" }`)
@ -1138,25 +1151,20 @@ func TestIntentionList_acl(t *testing.T) {
func TestIntentionMatch_good(t *testing.T) { func TestIntentionMatch_good(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create some records // Create some records
{ {
insert := [][]string{ insert := [][]string{
{"foo", "*", "foo", "*"}, {"default", "*", "default", "*"},
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "bar"},
{"foo", "*", "foo", "baz"}, // shouldn't match {"default", "*", "default", "baz"}, // shouldn't match
{"foo", "*", "bar", "bar"}, // shouldn't match
{"foo", "*", "bar", "*"}, // shouldn't match
{"foo", "*", "*", "*"},
{"bar", "*", "foo", "bar"}, // duplicate destination different source
} }
for _, v := range insert { for _, v := range insert {
@ -1174,7 +1182,7 @@ func TestIntentionMatch_good(t *testing.T) {
// Create // Create
var reply string var reply string
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
} }
} }
@ -1184,22 +1192,17 @@ func TestIntentionMatch_good(t *testing.T) {
Match: &structs.IntentionQueryMatch{ Match: &structs.IntentionQueryMatch{
Type: structs.IntentionMatchDestination, Type: structs.IntentionMatchDestination,
Entries: []structs.IntentionMatchEntry{ Entries: []structs.IntentionMatchEntry{
{ {Name: "bar"},
Namespace: "foo",
Name: "bar",
},
}, },
}, },
} }
var resp structs.IndexedIntentionMatches var resp structs.IndexedIntentionMatches
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)) require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
assert.Len(resp.Matches, 1) require.Len(t, resp.Matches, 1)
expected := [][]string{ expected := [][]string{
{"bar", "*", "foo", "bar"}, {"default", "*", "default", "bar"},
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "*"},
{"foo", "*", "foo", "*"},
{"foo", "*", "*", "*"},
} }
var actual [][]string var actual [][]string
for _, ixn := range resp.Matches[0] { for _, ixn := range resp.Matches[0] {
@ -1210,7 +1213,7 @@ func TestIntentionMatch_good(t *testing.T) {
ixn.DestinationName, ixn.DestinationName,
}) })
} }
assert.Equal(expected, actual) require.Equal(t, expected, actual)
} }
// Test matching with ACLs // Test matching with ACLs
@ -1299,36 +1302,32 @@ func TestIntentionMatch_acl(t *testing.T) {
func TestIntentionCheck_defaultNoACL(t *testing.T) { func TestIntentionCheck_defaultNoACL(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Test // Test
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{ Check: &structs.IntentionQueryCheck{
SourceNS: "foo",
SourceName: "bar", SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux", DestinationName: "qux",
SourceType: structs.IntentionSourceConsul, SourceType: structs.IntentionSourceConsul,
}, },
} }
var resp structs.IntentionQueryCheckResponse var resp structs.IntentionQueryCheckResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.True(resp.Allowed) require.True(t, resp.Allowed)
} }
// Test the Check method defaults to deny with allowlist ACLs. // Test the Check method defaults to deny with allowlist ACLs.
func TestIntentionCheck_defaultACLDeny(t *testing.T) { func TestIntentionCheck_defaultACLDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -1340,30 +1339,27 @@ func TestIntentionCheck_defaultACLDeny(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Check // Check
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{ Check: &structs.IntentionQueryCheck{
SourceNS: "foo",
SourceName: "bar", SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux", DestinationName: "qux",
SourceType: structs.IntentionSourceConsul, SourceType: structs.IntentionSourceConsul,
}, },
} }
req.Token = "root" req.Token = "root"
var resp structs.IntentionQueryCheckResponse var resp structs.IntentionQueryCheckResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.False(resp.Allowed) require.False(t, resp.Allowed)
} }
// Test the Check method defaults to deny with denylist ACLs. // Test the Check method defaults to deny with denylist ACLs.
func TestIntentionCheck_defaultACLAllow(t *testing.T) { func TestIntentionCheck_defaultACLAllow(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -1375,30 +1371,27 @@ func TestIntentionCheck_defaultACLAllow(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Check // Check
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{ Check: &structs.IntentionQueryCheck{
SourceNS: "foo",
SourceName: "bar", SourceName: "bar",
DestinationNS: "foo",
DestinationName: "qux", DestinationName: "qux",
SourceType: structs.IntentionSourceConsul, SourceType: structs.IntentionSourceConsul,
}, },
} }
req.Token = "root" req.Token = "root"
var resp structs.IntentionQueryCheckResponse var resp structs.IntentionQueryCheckResponse
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.True(resp.Allowed) require.True(t, resp.Allowed)
} }
// Test the Check method requires service:read permission. // Test the Check method requires service:read permission.
func TestIntentionCheck_aclDeny(t *testing.T) { func TestIntentionCheck_aclDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -1410,7 +1403,7 @@ func TestIntentionCheck_aclDeny(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") waitForLeaderEstablishment(t, s1)
// Create an ACL with service read permissions. This will grant permission. // Create an ACL with service read permissions. This will grant permission.
var token string var token string
@ -1430,16 +1423,14 @@ service "bar" {
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
require.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
} }
// Check // Check
req := &structs.IntentionQueryRequest{ req := &structs.IntentionQueryRequest{
Datacenter: "dc1", Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{ Check: &structs.IntentionQueryCheck{
SourceNS: "foo",
SourceName: "qux", SourceName: "qux",
DestinationNS: "foo",
DestinationName: "baz", DestinationName: "baz",
SourceType: structs.IntentionSourceConsul, SourceType: structs.IntentionSourceConsul,
}, },
@ -1447,7 +1438,7 @@ service "bar" {
req.Token = token req.Token = token
var resp structs.IntentionQueryCheckResponse var resp structs.IntentionQueryCheckResponse
err := msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp) err := msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)
require.True(acl.IsErrPermissionDenied(err)) require.True(t, acl.IsErrPermissionDenied(err))
} }
// Test the Check method returns allow/deny properly. // Test the Check method returns allow/deny properly.

View File

@ -559,6 +559,7 @@ func (s *Server) startConnectLeader() {
s.leaderRoutineManager.Start(secondaryCARootWatchRoutineName, s.secondaryCARootWatch) s.leaderRoutineManager.Start(secondaryCARootWatchRoutineName, s.secondaryCARootWatch)
s.leaderRoutineManager.Start(intentionReplicationRoutineName, s.replicateIntentions) s.leaderRoutineManager.Start(intentionReplicationRoutineName, s.replicateIntentions)
s.leaderRoutineManager.Start(secondaryCertRenewWatchRoutineName, s.secondaryIntermediateCertRenewalWatch) s.leaderRoutineManager.Start(secondaryCertRenewWatchRoutineName, s.secondaryIntermediateCertRenewalWatch)
s.startConnectLeaderEnterprise()
} }
s.leaderRoutineManager.Start(caRootPruningRoutineName, s.runCARootPruning) s.leaderRoutineManager.Start(caRootPruningRoutineName, s.runCARootPruning)
@ -569,6 +570,7 @@ func (s *Server) stopConnectLeader() {
s.leaderRoutineManager.Stop(secondaryCARootWatchRoutineName) s.leaderRoutineManager.Stop(secondaryCARootWatchRoutineName)
s.leaderRoutineManager.Stop(intentionReplicationRoutineName) s.leaderRoutineManager.Stop(intentionReplicationRoutineName)
s.leaderRoutineManager.Stop(caRootPruningRoutineName) s.leaderRoutineManager.Stop(caRootPruningRoutineName)
s.stopConnectLeaderEnterprise()
} }
func (s *Server) runCARootPruning(ctx context.Context) error { func (s *Server) runCARootPruning(ctx context.Context) error {
@ -789,7 +791,7 @@ func (s *Server) replicateIntentions(ctx context.Context) error {
return err return err
} }
_, local, err := s.fsm.State().Intentions(nil) _, local, err := s.fsm.State().Intentions(nil, s.replicationEnterpriseMeta())
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,141 @@
// +build !consulent
package consul
import (
"context"
"fmt"
"strings"
"time"
"github.com/hashicorp/consul/agent/structs"
)
const intentionUpgradeCleanupRoutineName = "intention cleanup"
func (s *Server) startConnectLeaderEnterprise() {
if s.config.PrimaryDatacenter != s.config.Datacenter {
// intention cleanup should only run in the primary
return
}
s.leaderRoutineManager.Start(intentionUpgradeCleanupRoutineName, s.runIntentionUpgradeCleanup)
}
func (s *Server) stopConnectLeaderEnterprise() {
// will be a no-op when not started
s.leaderRoutineManager.Stop(intentionUpgradeCleanupRoutineName)
}
func (s *Server) runIntentionUpgradeCleanup(ctx context.Context) error {
// TODO(rb): handle retry?
// Remove any intention in OSS that happened to have used a non-default
// namespace.
//
// The one exception is that if we find wildcards namespaces we "upgrade"
// them to "default" if there isn't already an existing intention.
_, ixns, err := s.fsm.State().Intentions(nil, structs.WildcardEnterpriseMeta())
if err != nil {
return fmt.Errorf("failed to list intentions: %v", err)
}
// default/<foo> => default/<foo> || OK
// default/* => default/<foo> || OK
// */* => default/<foo> || becomes: default/* => default/<foo>
// default/<foo> => default/* || OK
// default/* => default/* || OK
// */* => default/* || becomes: default/* => default/*
// default/<foo> => */* || becomes: default/<foo> => default/*
// default/* => */* || becomes: default/* => default/*
// */* => */* || becomes: default/* => default/*
type intentionName struct {
SourceNS, SourceName string
DestinationNS, DestinationName string
}
var (
retained = make(map[intentionName]struct{})
tryUpgrades = make(map[intentionName]*structs.Intention)
removeIDs []string
)
for _, ixn := range ixns {
srcNS := strings.ToLower(ixn.SourceNS)
if srcNS == "" {
srcNS = structs.IntentionDefaultNamespace
}
dstNS := strings.ToLower(ixn.DestinationNS)
if dstNS == "" {
dstNS = structs.IntentionDefaultNamespace
}
if srcNS == structs.IntentionDefaultNamespace && dstNS == structs.IntentionDefaultNamespace {
name := intentionName{
srcNS, ixn.SourceName,
dstNS, ixn.DestinationName,
}
retained[name] = struct{}{}
continue // a-ok for OSS
}
// If anything is wildcarded, attempt to reify it as "default".
if srcNS == structs.WildcardSpecifier || dstNS == structs.WildcardSpecifier {
updated := ixn.Clone()
if srcNS == structs.WildcardSpecifier {
updated.SourceNS = structs.IntentionDefaultNamespace
}
if dstNS == structs.WildcardSpecifier {
updated.DestinationNS = structs.IntentionDefaultNamespace
}
// Run parts of the checks in Intention.prepareApplyUpdate.
// We always update the updatedat field.
updated.UpdatedAt = time.Now().UTC()
// Set the precedence
updated.UpdatePrecedence()
// make sure we set the hash prior to raft application
updated.SetHash()
name := intentionName{
updated.SourceNS, updated.SourceName,
updated.DestinationNS, updated.DestinationName,
}
tryUpgrades[name] = updated
} else {
removeIDs = append(removeIDs, ixn.ID)
}
}
for name, updated := range tryUpgrades {
if _, collision := retained[name]; collision {
// The update we wanted to do would collide with an existing intention
// so delete our original wildcard intention instead.
removeIDs = append(removeIDs, updated.ID)
} else {
req := structs.IntentionRequest{
Op: structs.IntentionOpUpdate,
Intention: updated,
}
if _, err := s.raftApply(structs.IntentionRequestType, &req); err != nil {
return fmt.Errorf("failed to remove wildcard namespaces from intention %q: %v", updated.ID, err)
}
}
}
for _, id := range removeIDs {
req := structs.IntentionRequest{
Op: structs.IntentionOpDelete,
Intention: &structs.Intention{
ID: id,
},
}
if _, err := s.raftApply(structs.IntentionRequestType, &req); err != nil {
return fmt.Errorf("failed to remove intention with invalid namespace %q: %v", id, err)
}
}
return nil // transition complete
}

View File

@ -0,0 +1,180 @@
// +build !consulent
package consul
import (
"context"
"os"
"testing"
"time"
"github.com/hashicorp/consul/agent/structs"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/stretchr/testify/require"
)
func TestLeader_OSS_IntentionUpgradeCleanup(t *testing.T) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc1"
c.ACLDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
// set the build to ensure all the version checks pass and enable all the connect features that operate cross-dc
c.Build = "1.6.0"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
waitForLeaderEstablishment(t, s1)
s1.tokens.UpdateAgentToken("root", tokenStore.TokenSourceConfig)
lastIndex := uint64(0)
nextIndex := func() uint64 {
lastIndex++
return lastIndex
}
wildEntMeta := structs.WildcardEnterpriseMeta()
resetIntentions := func(t *testing.T) {
t.Helper()
_, ixns, err := s1.fsm.State().Intentions(nil, wildEntMeta)
require.NoError(t, err)
for _, ixn := range ixns {
require.NoError(t, s1.fsm.State().IntentionDelete(nextIndex(), ixn.ID))
}
}
compare := func(t *testing.T, expect [][]string) {
t.Helper()
_, ixns, err := s1.fsm.State().Intentions(nil, wildEntMeta)
require.NoError(t, err)
var actual [][]string
for _, ixn := range ixns {
actual = append(actual, []string{
ixn.SourceNS,
ixn.SourceName,
ixn.DestinationNS,
ixn.DestinationName,
})
}
require.ElementsMatch(t, expect, actual)
}
type testCase struct {
insert [][]string
expect [][]string
}
cases := map[string]testCase{
"no change": {
insert: [][]string{
{"default", "foo", "default", "bar"},
{"default", "*", "default", "*"},
},
expect: [][]string{
{"default", "foo", "default", "bar"},
{"default", "*", "default", "*"},
},
},
"non-wildcard deletions": {
insert: [][]string{
{"default", "foo", "default", "bar"},
{"alpha", "*", "default", "bar"},
{"default", "foo", "beta", "*"},
{"alpha", "zoo", "beta", "bar"},
},
expect: [][]string{
{"default", "foo", "default", "bar"},
},
},
"updates with no deletions and no collisions": {
insert: [][]string{
{"default", "foo", "default", "bar"},
{"default", "foo", "*", "*"},
{"*", "*", "default", "bar"},
{"*", "*", "*", "*"},
},
expect: [][]string{
{"default", "foo", "default", "bar"},
{"default", "foo", "default", "*"},
{"default", "*", "default", "bar"},
{"default", "*", "default", "*"},
},
},
"updates with only collision deletions": {
insert: [][]string{
{"default", "foo", "default", "bar"},
{"default", "foo", "default", "*"},
{"default", "foo", "*", "*"},
{"default", "*", "default", "bar"},
{"*", "*", "default", "bar"},
{"default", "*", "default", "*"},
{"*", "*", "*", "*"},
},
expect: [][]string{
{"default", "foo", "default", "bar"},
{"default", "foo", "default", "*"},
{"default", "*", "default", "bar"},
{"default", "*", "default", "*"},
},
},
"a bit of everything": {
insert: [][]string{
{"default", "foo", "default", "bar"}, // retained
{"default", "foo", "*", "*"}, // upgrade
{"default", "*", "default", "bar"}, // retained in collision
{"*", "*", "default", "bar"}, // deleted in collision
{"default", "*", "default", "*"}, // retained in collision
{"*", "*", "*", "*"}, // deleted in collision
{"alpha", "*", "default", "bar"}, // deleted
{"default", "foo", "beta", "*"}, // deleted
{"alpha", "zoo", "beta", "bar"}, // deleted
},
expect: [][]string{
{"default", "foo", "default", "bar"},
{"default", "foo", "default", "*"},
{"default", "*", "default", "bar"},
{"default", "*", "default", "*"},
},
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
resetIntentions(t)
// Do something super evil and directly reach into the FSM to seed it with "bad" data.
for _, elem := range tc.insert {
require.Len(t, elem, 4)
ixn := structs.TestIntention(t)
ixn.ID = generateUUID()
ixn.SourceNS = elem[0]
ixn.SourceName = elem[1]
ixn.DestinationNS = elem[2]
ixn.DestinationName = elem[3]
ixn.CreatedAt = time.Now().UTC()
ixn.UpdatedAt = ixn.CreatedAt
require.NoError(t, s1.fsm.State().IntentionSet(nextIndex(), ixn))
}
// Sleep a bit so that the UpdatedAt field will definitely be different
time.Sleep(1 * time.Millisecond)
// TODO: figure out how to test this properly during leader startup
require.NoError(t, s1.runIntentionUpgradeCleanup(context.Background()))
compare(t, tc.expect)
})
}
}

View File

@ -126,7 +126,7 @@ func (s *Restore) Intention(ixn *structs.Intention) error {
} }
// Intentions returns the list of all intentions. // Intentions returns the list of all intentions.
func (s *Store) Intentions(ws memdb.WatchSet) (uint64, structs.Intentions, error) { func (s *Store) Intentions(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.Intentions, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -136,11 +136,11 @@ func (s *Store) Intentions(ws memdb.WatchSet) (uint64, structs.Intentions, error
idx = 1 idx = 1
} }
// Get all intentions iter, err := s.intentionListTxn(tx, entMeta)
iter, err := tx.Get(intentionsTableName, "id")
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("failed intention lookup: %s", err) return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
} }
ws.Add(iter.WatchCh()) ws.Add(iter.WatchCh())
var results structs.Intentions var results structs.Intentions
@ -250,6 +250,38 @@ func (s *Store) IntentionGet(ws memdb.WatchSet, id string) (uint64, *structs.Int
return idx, result, nil return idx, result, nil
} }
// IntentionGetExact returns the given intention by it's full unique name.
func (s *Store) IntentionGetExact(ws memdb.WatchSet, args *structs.IntentionQueryExact) (uint64, *structs.Intention, error) {
tx := s.db.Txn(false)
defer tx.Abort()
if err := args.Validate(); err != nil {
return 0, nil, err
}
// Get the table index.
idx := maxIndexTxn(tx, intentionsTableName)
if idx < 1 {
idx = 1
}
// Look up by its full name.
watchCh, intention, err := tx.FirstWatch(intentionsTableName, "source_destination",
args.SourceNS, args.SourceName, args.DestinationNS, args.DestinationName)
if err != nil {
return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
}
ws.Add(watchCh)
// Convert the interface{} if it is non-nil
var result *structs.Intention
if intention != nil {
result = intention.(*structs.Intention)
}
return idx, result, nil
}
// IntentionDelete deletes the given intention by ID. // IntentionDelete deletes the given intention by ID.
func (s *Store) IntentionDelete(idx uint64, id string) error { func (s *Store) IntentionDelete(idx uint64, id string) error {
tx := s.db.WriteTxn(idx) tx := s.db.WriteTxn(idx)

View File

@ -0,0 +1,13 @@
// +build !consulent
package state
import (
"github.com/hashicorp/consul/agent/structs"
memdb "github.com/hashicorp/go-memdb"
)
func (s *Store) intentionListTxn(tx *txn, _ *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
// Get all intentions
return tx.Get(intentionsTableName, "id")
}

View File

@ -223,64 +223,65 @@ func TestStore_IntentionDelete(t *testing.T) {
} }
func TestStore_IntentionsList(t *testing.T) { func TestStore_IntentionsList(t *testing.T) {
assert := assert.New(t)
s := testStateStore(t) s := testStateStore(t)
entMeta := structs.WildcardEnterpriseMeta()
// Querying with no results returns nil. // Querying with no results returns nil.
ws := memdb.NewWatchSet() ws := memdb.NewWatchSet()
idx, res, err := s.Intentions(ws) idx, res, err := s.Intentions(ws, entMeta)
assert.NoError(err) require.NoError(t, err)
assert.Nil(res) require.Nil(t, res)
assert.Equal(uint64(1), idx) require.Equal(t, uint64(1), idx)
testIntention := func(srcNS, src, dstNS, dst string) *structs.Intention {
id := testUUID()
return &structs.Intention{
ID: id,
SourceNS: srcNS,
SourceName: src,
DestinationNS: dstNS,
DestinationName: dst,
Meta: map[string]string{},
}
}
cmpIntention := func(ixn *structs.Intention, id string, index uint64) *structs.Intention {
ixn.ID = id
ixn.CreateIndex = index
ixn.ModifyIndex = index
ixn.UpdatePrecedence() // to match what is returned...
return ixn
}
// Create some intentions // Create some intentions
ixns := structs.Intentions{ ixns := structs.Intentions{
&structs.Intention{ testIntention("default", "foo", "default", "bar"),
ID: testUUID(), testIntention("default", "foo", "default", "*"),
Meta: map[string]string{}, testIntention("*", "*", "default", "*"),
}, testIntention("default", "*", "*", "*"),
&structs.Intention{ testIntention("*", "*", "*", "*"),
ID: testUUID(),
Meta: map[string]string{},
},
} }
// Force deterministic sort order
ixns[0].ID = "a" + ixns[0].ID[1:]
ixns[1].ID = "b" + ixns[1].ID[1:]
// Create // Create
for i, ixn := range ixns { for i, ixn := range ixns {
assert.NoError(s.IntentionSet(uint64(1+i), ixn)) require.NoError(t, s.IntentionSet(uint64(1+i), ixn))
} }
assert.True(watchFired(ws), "watch fired") require.True(t, watchFired(ws), "watch fired")
// Read it back and verify. // Read it back and verify.
expected := structs.Intentions{ expected := structs.Intentions{
&structs.Intention{ cmpIntention(testIntention("default", "foo", "default", "bar"), ixns[0].ID, 1),
ID: ixns[0].ID, cmpIntention(testIntention("default", "foo", "default", "*"), ixns[1].ID, 2),
Meta: map[string]string{}, cmpIntention(testIntention("*", "*", "default", "*"), ixns[2].ID, 3),
RaftIndex: structs.RaftIndex{ cmpIntention(testIntention("default", "*", "*", "*"), ixns[3].ID, 4),
CreateIndex: 1, cmpIntention(testIntention("*", "*", "*", "*"), ixns[4].ID, 5),
ModifyIndex: 1,
},
},
&structs.Intention{
ID: ixns[1].ID,
Meta: map[string]string{},
RaftIndex: structs.RaftIndex{
CreateIndex: 2,
ModifyIndex: 2,
},
},
} }
for i := range expected {
expected[i].UpdatePrecedence() // to match what is returned... idx, actual, err := s.Intentions(nil, entMeta)
} require.NoError(t, err)
idx, actual, err := s.Intentions(nil) require.Equal(t, idx, uint64(5))
assert.NoError(err) require.ElementsMatch(t, expected, actual)
assert.Equal(idx, uint64(2))
assert.Equal(expected, actual)
} }
// Test the matrix of match logic. // Test the matrix of match logic.
@ -551,7 +552,8 @@ func TestStore_Intention_Snapshot_Restore(t *testing.T) {
// Intentions are returned precedence sorted unlike the snapshot so we need // Intentions are returned precedence sorted unlike the snapshot so we need
// to rearrange the expected slice some. // to rearrange the expected slice some.
expected[0], expected[1], expected[2] = expected[1], expected[2], expected[0] expected[0], expected[1], expected[2] = expected[1], expected[2], expected[0]
idx, actual, err := s.Intentions(nil) entMeta := structs.WildcardEnterpriseMeta()
idx, actual, err := s.Intentions(nil, entMeta)
assert.NoError(err) assert.NoError(err)
assert.Equal(idx, uint64(6)) assert.Equal(idx, uint64(6))
assert.Equal(expected, actual) assert.Equal(expected, actual)

View File

@ -77,7 +77,7 @@ func TestStateStore_Txn_Intention(t *testing.T) {
require.Equal(t, expected, results) require.Equal(t, expected, results)
// Pull the resulting state store contents. // Pull the resulting state store contents.
idx, actual, err := s.Intentions(nil) idx, actual, err := s.Intentions(nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(3), idx, "wrong index") require.Equal(t, uint64(3), idx, "wrong index")

View File

@ -20,6 +20,18 @@ func (s *HTTPServer) parseEntMeta(req *http.Request, entMeta *structs.Enterprise
return nil return nil
} }
func (s *HTTPServer) validateEnterpriseIntentionNamespace(logName, ns string, _ bool) error {
if ns == "" {
return nil
} else if strings.ToLower(ns) == structs.IntentionDefaultNamespace {
return nil
}
// No special handling for wildcard namespaces as they are pointless in OSS.
return BadRequestError{Reason: "Invalid " + logName + "(" + ns + ")" + ": Namespaces is a Consul Enterprise feature"}
}
func (s *HTTPServer) parseEntMetaNoWildcard(req *http.Request, _ *structs.EnterpriseMeta) error { func (s *HTTPServer) parseEntMetaNoWildcard(req *http.Request, _ *structs.EnterpriseMeta) error {
return s.parseEntMeta(req, nil) return s.parseEntMeta(req, nil)
} }

View File

@ -76,6 +76,7 @@ func init() {
registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionEndpoint) registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionEndpoint)
registerEndpoint("/v1/connect/intentions/match", []string{"GET"}, (*HTTPServer).IntentionMatch) registerEndpoint("/v1/connect/intentions/match", []string{"GET"}, (*HTTPServer).IntentionMatch)
registerEndpoint("/v1/connect/intentions/check", []string{"GET"}, (*HTTPServer).IntentionCheck) registerEndpoint("/v1/connect/intentions/check", []string{"GET"}, (*HTTPServer).IntentionCheck)
registerEndpoint("/v1/connect/intentions/exact", []string{"GET"}, (*HTTPServer).IntentionGetExact)
registerEndpoint("/v1/connect/intentions/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).IntentionSpecific) registerEndpoint("/v1/connect/intentions/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).IntentionSpecific)
registerEndpoint("/v1/coordinate/datacenters", []string{"GET"}, (*HTTPServer).CoordinateDatacenters) registerEndpoint("/v1/coordinate/datacenters", []string{"GET"}, (*HTTPServer).CoordinateDatacenters)
registerEndpoint("/v1/coordinate/nodes", []string{"GET"}, (*HTTPServer).CoordinateNodes) registerEndpoint("/v1/coordinate/nodes", []string{"GET"}, (*HTTPServer).CoordinateNodes)

View File

@ -9,7 +9,7 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
) )
// /v1/connection/intentions // /v1/connect/intentions
func (s *HTTPServer) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
switch req.Method { switch req.Method {
case "GET": case "GET":
@ -32,6 +32,10 @@ func (s *HTTPServer) IntentionList(resp http.ResponseWriter, req *http.Request)
return nil, nil return nil, nil
} }
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var reply structs.IndexedIntentions var reply structs.IndexedIntentions
defer setMeta(resp, &reply.QueryMeta) defer setMeta(resp, &reply.QueryMeta)
if err := s.agent.RPC("Intention.List", &args, &reply); err != nil { if err := s.agent.RPC("Intention.List", &args, &reply); err != nil {
@ -45,6 +49,11 @@ func (s *HTTPServer) IntentionList(resp http.ResponseWriter, req *http.Request)
func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Method is tested in IntentionEndpoint // Method is tested in IntentionEndpoint
var entMeta structs.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}
args := structs.IntentionRequest{ args := structs.IntentionRequest{
Op: structs.IntentionOpCreate, Op: structs.IntentionOpCreate,
} }
@ -54,6 +63,12 @@ func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request
return nil, fmt.Errorf("Failed to decode request body: %s", err) return nil, fmt.Errorf("Failed to decode request body: %s", err)
} }
args.Intention.FillNonDefaultNamespaces(&entMeta)
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return nil, err
}
var reply string var reply string
if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil { if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
return nil, err return nil, err
@ -62,6 +77,16 @@ func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request
return intentionCreateResponse{reply}, nil return intentionCreateResponse{reply}, nil
} }
func (s *HTTPServer) validateEnterpriseIntention(ixn *structs.Intention) error {
if err := s.validateEnterpriseIntentionNamespace("SourceNS", ixn.SourceNS, true); err != nil {
return err
}
if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil {
return err
}
return nil
}
// GET /v1/connect/intentions/match // GET /v1/connect/intentions/match
func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Prepare args // Prepare args
@ -70,6 +95,11 @@ func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request)
return nil, nil return nil, nil
} }
var entMeta structs.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}
q := req.URL.Query() q := req.URL.Query()
// Extract the "by" query parameter // Extract the "by" query parameter
@ -94,7 +124,7 @@ func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request)
// order of the returned responses. // order of the returned responses.
args.Match.Entries = make([]structs.IntentionMatchEntry, len(names)) args.Match.Entries = make([]structs.IntentionMatchEntry, len(names))
for i, n := range names { for i, n := range names {
entry, err := parseIntentionMatchEntry(n) entry, err := parseIntentionMatchEntry(n, &entMeta)
if err != nil { if err != nil {
return nil, fmt.Errorf("name %q is invalid: %s", n, err) return nil, fmt.Errorf("name %q is invalid: %s", n, err)
} }
@ -129,6 +159,11 @@ func (s *HTTPServer) IntentionCheck(resp http.ResponseWriter, req *http.Request)
return nil, nil return nil, nil
} }
var entMeta structs.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}
q := req.URL.Query() q := req.URL.Query()
// Set the source type if set // Set the source type if set
@ -150,7 +185,7 @@ func (s *HTTPServer) IntentionCheck(resp http.ResponseWriter, req *http.Request)
// We parse them the same way as matches to extract namespace/name // We parse them the same way as matches to extract namespace/name
args.Check.SourceName = source[0] args.Check.SourceName = source[0]
if args.Check.SourceType == structs.IntentionSourceConsul { if args.Check.SourceType == structs.IntentionSourceConsul {
entry, err := parseIntentionMatchEntry(source[0]) entry, err := parseIntentionMatchEntry(source[0], &entMeta)
if err != nil { if err != nil {
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
} }
@ -159,7 +194,7 @@ func (s *HTTPServer) IntentionCheck(resp http.ResponseWriter, req *http.Request)
} }
// The destination is always in the Consul format // The destination is always in the Consul format
entry, err := parseIntentionMatchEntry(destination[0]) entry, err := parseIntentionMatchEntry(destination[0], &entMeta)
if err != nil { if err != nil {
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
} }
@ -174,7 +209,81 @@ func (s *HTTPServer) IntentionCheck(resp http.ResponseWriter, req *http.Request)
return &reply, nil return &reply, nil
} }
// IntentionSpecific handles the endpoint for /v1/connection/intentions/:id // GET /v1/connect/intentions/exact
func (s *HTTPServer) IntentionGetExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var entMeta structs.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}
args := structs.IntentionQueryRequest{
Exact: &structs.IntentionQueryExact{},
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
q := req.URL.Query()
// Extract the source/destination
source, ok := q["source"]
if !ok || len(source) != 1 {
return nil, fmt.Errorf("required query parameter 'source' not set")
}
destination, ok := q["destination"]
if !ok || len(destination) != 1 {
return nil, fmt.Errorf("required query parameter 'destination' not set")
}
{
entry, err := parseIntentionMatchEntry(source[0], &entMeta)
if err != nil {
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
}
args.Exact.SourceNS = entry.Namespace
args.Exact.SourceName = entry.Name
}
{
entry, err := parseIntentionMatchEntry(destination[0], &entMeta)
if err != nil {
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
}
args.Exact.DestinationNS = entry.Namespace
args.Exact.DestinationName = entry.Name
}
var reply structs.IndexedIntentions
if err := s.agent.RPC("Intention.Get", &args, &reply); err != nil {
// We have to check the string since the RPC sheds the error type
if err.Error() == consul.ErrIntentionNotFound.Error() {
resp.WriteHeader(http.StatusNotFound)
fmt.Fprint(resp, err.Error())
return nil, nil
}
// Not ideal, but there are a number of error scenarios that are not
// user error (400). We look for a specific case of invalid UUID
// to detect a parameter error and return a 400 response. The error
// is not a constant type or message, so we have to use strings.Contains
if strings.Contains(err.Error(), "UUID") {
return nil, BadRequestError{Reason: err.Error()}
}
return nil, err
}
// This shouldn't happen since the called API documents it shouldn't,
// but we check since the alternative if it happens is a panic.
if len(reply.Intentions) == 0 {
resp.WriteHeader(http.StatusNotFound)
return nil, nil
}
return reply.Intentions[0], nil
}
// IntentionSpecific handles the endpoint for /v1/connect/intentions/:id
func (s *HTTPServer) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/") id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/")
@ -238,6 +347,11 @@ func (s *HTTPServer) IntentionSpecificGet(id string, resp http.ResponseWriter, r
func (s *HTTPServer) IntentionSpecificUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) IntentionSpecificUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Method is tested in IntentionEndpoint // Method is tested in IntentionEndpoint
var entMeta structs.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}
args := structs.IntentionRequest{ args := structs.IntentionRequest{
Op: structs.IntentionOpUpdate, Op: structs.IntentionOpUpdate,
} }
@ -247,6 +361,8 @@ func (s *HTTPServer) IntentionSpecificUpdate(id string, resp http.ResponseWriter
return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)} return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)}
} }
args.Intention.FillNonDefaultNamespaces(&entMeta)
// Use the ID from the URL // Use the ID from the URL
args.Intention.ID = id args.Intention.ID = id
@ -284,9 +400,9 @@ type intentionCreateResponse struct{ ID string }
// parseIntentionMatchEntry parses the query parameter for an intention // parseIntentionMatchEntry parses the query parameter for an intention
// match query entry. // match query entry.
func parseIntentionMatchEntry(input string) (structs.IntentionMatchEntry, error) { func parseIntentionMatchEntry(input string, entMeta *structs.EnterpriseMeta) (structs.IntentionMatchEntry, error) {
var result structs.IntentionMatchEntry var result structs.IntentionMatchEntry
result.Namespace = structs.IntentionDefaultNamespace result.Namespace = entMeta.NamespaceOrEmpty()
// Get the index to the '/'. If it doesn't exist, we have just a name // Get the index to the '/'. If it doesn't exist, we have just a name
// so just set that and return. // so just set that and return.

View File

@ -0,0 +1,48 @@
// +build !consulent
package agent
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestOSS_IntentionsCreate_failure(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, "")
defer a.Shutdown()
doCreate := func(t *testing.T, srcNS, dstNS string) {
t.Helper()
args := structs.TestIntention(t)
args.SourceNS = srcNS
args.SourceName = "*"
args.DestinationNS = dstNS
args.DestinationName = "*"
req, _ := http.NewRequest("POST", "/v1/connect/intentions", jsonReader(args))
resp := httptest.NewRecorder()
_, err := a.srv.IntentionCreate(resp, req)
require.Error(t, err)
}
t.Run("wildcard source namespace", func(t *testing.T) {
doCreate(t, "*", "default")
})
t.Run("wildcard destination namespace", func(t *testing.T) {
doCreate(t, "default", "*")
})
t.Run("wildcard source and destination namespaces", func(t *testing.T) {
doCreate(t, "*", "*")
})
t.Run("non-default source namespace", func(t *testing.T) {
doCreate(t, "foo", "default")
})
t.Run("non-default destination namespace", func(t *testing.T) {
doCreate(t, "default", "foo")
})
}

View File

@ -71,20 +71,15 @@ func TestIntentionsList_values(t *testing.T) {
func TestIntentionsMatch_basic(t *testing.T) { func TestIntentionsMatch_basic(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t)
a := NewTestAgent(t, "") a := NewTestAgent(t, "")
defer a.Shutdown() defer a.Shutdown()
// Create some intentions // Create some intentions
{ {
insert := [][]string{ insert := [][]string{
{"foo", "*", "foo", "*"}, {"default", "*", "default", "*"},
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "bar"},
{"foo", "*", "foo", "baz"}, // shouldn't match {"default", "*", "default", "baz"}, // shouldn't match
{"foo", "*", "bar", "bar"}, // shouldn't match
{"foo", "*", "bar", "*"}, // shouldn't match
{"foo", "*", "*", "*"},
{"bar", "*", "foo", "bar"}, // duplicate destination different source
} }
for _, v := range insert { for _, v := range insert {
@ -100,28 +95,26 @@ func TestIntentionsMatch_basic(t *testing.T) {
// Create // Create
var reply string var reply string
assert.Nil(a.RPC("Intention.Apply", &ixn, &reply)) require.Nil(t, a.RPC("Intention.Apply", &ixn, &reply))
} }
} }
// Request // Request
req, _ := http.NewRequest("GET", req, _ := http.NewRequest("GET",
"/v1/connect/intentions/match?by=destination&name=foo/bar", nil) "/v1/connect/intentions/match?by=destination&name=bar", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.IntentionMatch(resp, req) obj, err := a.srv.IntentionMatch(resp, req)
assert.Nil(err) require.Nil(t, err)
value := obj.(map[string]structs.Intentions) value := obj.(map[string]structs.Intentions)
assert.Len(value, 1) require.Len(t, value, 1)
var actual [][]string var actual [][]string
expected := [][]string{ expected := [][]string{
{"bar", "*", "foo", "bar"}, {"default", "*", "default", "bar"},
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "*"},
{"foo", "*", "foo", "*"},
{"foo", "*", "*", "*"},
} }
for _, ixn := range value["foo/bar"] { for _, ixn := range value["bar"] {
actual = append(actual, []string{ actual = append(actual, []string{
ixn.SourceNS, ixn.SourceNS,
ixn.SourceName, ixn.SourceName,
@ -130,7 +123,7 @@ func TestIntentionsMatch_basic(t *testing.T) {
}) })
} }
assert.Equal(expected, actual) require.Equal(t, expected, actual)
} }
func TestIntentionsMatch_noBy(t *testing.T) { func TestIntentionsMatch_noBy(t *testing.T) {
@ -187,16 +180,14 @@ func TestIntentionsMatch_noName(t *testing.T) {
func TestIntentionsCheck_basic(t *testing.T) { func TestIntentionsCheck_basic(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
a := NewTestAgent(t, "") a := NewTestAgent(t, "")
defer a.Shutdown() defer a.Shutdown()
// Create some intentions // Create some intentions
{ {
insert := [][]string{ insert := [][]string{
{"foo", "*", "foo", "*"}, {"default", "*", "default", "baz"},
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "bar"},
{"bar", "*", "foo", "bar"},
} }
for _, v := range insert { for _, v := range insert {
@ -213,30 +204,30 @@ func TestIntentionsCheck_basic(t *testing.T) {
// Create // Create
var reply string var reply string
require.Nil(a.RPC("Intention.Apply", &ixn, &reply)) require.NoError(t, a.RPC("Intention.Apply", &ixn, &reply))
} }
} }
// Request matching intention // Request matching intention
{ {
req, _ := http.NewRequest("GET", req, _ := http.NewRequest("GET",
"/v1/connect/intentions/test?source=foo/bar&destination=foo/baz", nil) "/v1/connect/intentions/test?source=bar&destination=baz", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.IntentionCheck(resp, req) obj, err := a.srv.IntentionCheck(resp, req)
require.Nil(err) require.NoError(t, err)
value := obj.(*structs.IntentionQueryCheckResponse) value := obj.(*structs.IntentionQueryCheckResponse)
require.False(value.Allowed) require.False(t, value.Allowed)
} }
// Request non-matching intention // Request non-matching intention
{ {
req, _ := http.NewRequest("GET", req, _ := http.NewRequest("GET",
"/v1/connect/intentions/test?source=foo/bar&destination=bar/qux", nil) "/v1/connect/intentions/test?source=bar&destination=qux", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.IntentionCheck(resp, req) obj, err := a.srv.IntentionCheck(resp, req)
require.Nil(err) require.NoError(t, err)
value := obj.(*structs.IntentionQueryCheckResponse) value := obj.(*structs.IntentionQueryCheckResponse)
require.True(value.Allowed) require.True(t, value.Allowed)
} }
} }
@ -482,12 +473,10 @@ func TestParseIntentionMatchEntry(t *testing.T) {
{ {
"foo", "foo",
structs.IntentionMatchEntry{ structs.IntentionMatchEntry{
Namespace: structs.IntentionDefaultNamespace, Name: "foo",
Name: "foo",
}, },
false, false,
}, },
{ {
"foo/bar", "foo/bar",
structs.IntentionMatchEntry{ structs.IntentionMatchEntry{
@ -496,7 +485,6 @@ func TestParseIntentionMatchEntry(t *testing.T) {
}, },
false, false,
}, },
{ {
"foo/bar/baz", "foo/bar/baz",
structs.IntentionMatchEntry{}, structs.IntentionMatchEntry{},
@ -507,7 +495,8 @@ func TestParseIntentionMatchEntry(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.Input, func(t *testing.T) { t.Run(tc.Input, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
actual, err := parseIntentionMatchEntry(tc.Input) var entMeta structs.EnterpriseMeta
actual, err := parseIntentionMatchEntry(tc.Input, &entMeta)
assert.Equal(err != nil, tc.Err, err) assert.Equal(err != nil, tc.Err, err)
if err != nil { if err != nil {
return return

View File

@ -2,6 +2,7 @@ package structs
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"sort" "sort"
"strconv" "strconv"
@ -85,6 +86,18 @@ type Intention struct {
RaftIndex `bexpr:"-"` RaftIndex `bexpr:"-"`
} }
func (t *Intention) Clone() *Intention {
t2 := *t
if t.Meta != nil {
t2.Meta = make(map[string]string)
for k, v := range t.Meta {
t2.Meta[k] = v
}
}
t2.Hash = nil
return &t2
}
func (t *Intention) UnmarshalJSON(data []byte) (err error) { func (t *Intention) UnmarshalJSON(data []byte) (err error) {
type Alias Intention type Alias Intention
aux := &struct { aux := &struct {
@ -246,6 +259,10 @@ func (ixn *Intention) CanRead(authz acl.Authorizer) bool {
} }
var authzContext acl.AuthorizerContext var authzContext acl.AuthorizerContext
// Read access on either end of the intention allows you to read the
// complete intention. This is so that both ends can be aware of why
// something does or does not work.
if ixn.SourceName != "" { if ixn.SourceName != "" {
ixn.FillAuthzContext(&authzContext, false) ixn.FillAuthzContext(&authzContext, false)
if authz.IntentionRead(ixn.SourceName, &authzContext) == acl.Allow { if authz.IntentionRead(ixn.SourceName, &authzContext) == acl.Allow {
@ -431,6 +448,10 @@ type IntentionQueryRequest struct {
// return allowed/deny based on an exact match. // return allowed/deny based on an exact match.
Check *IntentionQueryCheck Check *IntentionQueryCheck
// Exact is non-nil if we're performing a lookup of an intention by its
// unique name instead of its ID.
Exact *IntentionQueryExact
// Options for queries // Options for queries
QueryOptions QueryOptions
} }
@ -507,6 +528,31 @@ type IntentionQueryCheckResponse struct {
Allowed bool Allowed bool
} }
// IntentionQueryExact holds the parameters for performing a lookup of an
// intention by its unique name instead of its ID.
type IntentionQueryExact struct {
SourceNS, SourceName string
DestinationNS, DestinationName string
}
// Validate is used to ensure all 4 parameters are specified.
func (q *IntentionQueryExact) Validate() error {
var err error
if q.SourceNS == "" {
err = multierror.Append(err, errors.New("SourceNS is missing"))
}
if q.SourceName == "" {
err = multierror.Append(err, errors.New("SourceName is missing"))
}
if q.DestinationNS == "" {
err = multierror.Append(err, errors.New("DestinationNS is missing"))
}
if q.DestinationName == "" {
err = multierror.Append(err, errors.New("DestinationName is missing"))
}
return err
}
// 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

View File

@ -38,3 +38,10 @@ func (ixn *Intention) DefaultNamespaces(_ *EnterpriseMeta) {
ixn.DestinationNS = IntentionDefaultNamespace ixn.DestinationNS = IntentionDefaultNamespace
} }
} }
// FillNonDefaultNamespaces will populate the SourceNS and DestinationNS fields
// if they are empty with the proper defaults, but only if the proper defaults
// are themselves not "default".
func (ixn *Intention) FillNonDefaultNamespaces(_ *EnterpriseMeta) {
// do nothing
}

View File

@ -46,6 +46,10 @@ func (m *EnterpriseMeta) NamespaceOrDefault() string {
return IntentionDefaultNamespace return IntentionDefaultNamespace
} }
func (m *EnterpriseMeta) NamespaceOrEmpty() string {
return ""
}
func EnterpriseMetaInitializer(_ string) EnterpriseMeta { func EnterpriseMetaInitializer(_ string) EnterpriseMeta {
return emptyEnterpriseMeta return emptyEnterpriseMeta
} }

View File

@ -270,6 +270,39 @@ func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *
return out.Allowed, qm, nil return out.Allowed, qm, nil
} }
// IntentionGetExact retrieves a single intention by its unique name instead of
// its ID.
func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) {
r := h.c.newRequest("GET", "/v1/connect/intentions/exact")
r.setQueryOptions(q)
r.params.Set("source", source)
r.params.Set("destination", destination)
rtt, resp, err := h.c.doRequest(r)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if resp.StatusCode == 404 {
return nil, qm, nil
} else if resp.StatusCode != 200 {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
return nil, nil, fmt.Errorf(
"Unexpected response %d: %s", resp.StatusCode, buf.String())
}
var out Intention
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// IntentionCreate will create a new intention. The ID in the given // IntentionCreate will create a new intention. The ID in the given
// structure must be empty and a generate ID will be returned on // structure must be empty and a generate ID will be returned on
// success. // success.

View File

@ -41,7 +41,7 @@ func TestAPI_ConnectIntentionCreateListGetUpdateDelete(t *testing.T) {
require.Equal(ixn, actual) require.Equal(ixn, actual)
// Update it // Update it
ixn.SourceNS = ixn.SourceNS + "-different" ixn.SourceName = ixn.SourceName + "-different"
_, err = connect.IntentionUpdate(ixn, nil) _, err = connect.IntentionUpdate(ixn, nil)
require.NoError(err) require.NoError(err)
@ -91,12 +91,9 @@ func TestAPI_ConnectIntentionMatch(t *testing.T) {
// Create // Create
{ {
insert := [][]string{ insert := [][]string{
{"foo", "*"}, {"default", "*"},
{"foo", "bar"}, {"default", "bar"},
{"foo", "baz"}, // shouldn't match {"default", "baz"}, // shouldn't match
{"bar", "bar"}, // shouldn't match
{"bar", "*"}, // shouldn't match
{"*", "*"},
} }
for _, v := range insert { for _, v := range insert {
@ -112,14 +109,17 @@ func TestAPI_ConnectIntentionMatch(t *testing.T) {
// Match it // Match it
result, _, err := connect.IntentionMatch(&IntentionMatch{ result, _, err := connect.IntentionMatch(&IntentionMatch{
By: IntentionMatchDestination, By: IntentionMatchDestination,
Names: []string{"foo/bar"}, Names: []string{"bar"},
}, nil) }, nil)
require.Nil(err) require.Nil(err)
require.Len(result, 1) require.Len(result, 1)
var actual [][]string var actual [][]string
expected := [][]string{{"foo", "bar"}, {"foo", "*"}, {"*", "*"}} expected := [][]string{
for _, ixn := range result["foo/bar"] { {"default", "bar"},
{"default", "*"},
}
for _, ixn := range result["bar"] {
actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName}) actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName})
} }
@ -138,7 +138,8 @@ func TestAPI_ConnectIntentionCheck(t *testing.T) {
// Create // Create
{ {
insert := [][]string{ insert := [][]string{
{"foo", "*", "foo", "bar"}, {"default", "*", "default", "bar", "deny"},
{"default", "foo", "default", "bar", "allow"},
} }
for _, v := range insert { for _, v := range insert {
@ -147,39 +148,39 @@ func TestAPI_ConnectIntentionCheck(t *testing.T) {
ixn.SourceName = v[1] ixn.SourceName = v[1]
ixn.DestinationNS = v[2] ixn.DestinationNS = v[2]
ixn.DestinationName = v[3] ixn.DestinationName = v[3]
ixn.Action = IntentionActionDeny ixn.Action = IntentionAction(v[4])
id, _, err := connect.IntentionCreate(ixn, nil) id, _, err := connect.IntentionCreate(ixn, nil)
require.Nil(err) require.Nil(err)
require.NotEmpty(id) require.NotEmpty(id)
} }
} }
// Match it // Match the deny rule
{ {
result, _, err := connect.IntentionCheck(&IntentionCheck{ result, _, err := connect.IntentionCheck(&IntentionCheck{
Source: "foo/qux", Source: "default/qux",
Destination: "foo/bar", Destination: "default/bar",
}, nil) }, nil)
require.Nil(err) require.NoError(err)
require.False(result) require.False(result)
} }
// Match it (non-matching) // Match the allow rule
{ {
result, _, err := connect.IntentionCheck(&IntentionCheck{ result, _, err := connect.IntentionCheck(&IntentionCheck{
Source: "bar/qux", Source: "default/foo",
Destination: "foo/bar", Destination: "default/bar",
}, nil) }, nil)
require.Nil(err) require.NoError(err)
require.True(result) require.True(result)
} }
} }
func testIntention() *Intention { func testIntention() *Intention {
return &Intention{ return &Intention{
SourceNS: "eng", SourceNS: "default",
SourceName: "api", SourceName: "api",
DestinationNS: "eng", DestinationNS: "default",
DestinationName: "db", DestinationName: "db",
Precedence: 9, Precedence: 9,
Action: IntentionActionAllow, Action: IntentionActionAllow,

View File

@ -10,7 +10,6 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/intention/create" "github.com/hashicorp/consul/command/intention/create"
"github.com/hashicorp/consul/command/intention/finder"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -183,8 +182,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Output(fmt.Sprintf("Successfully updated config entry for ingress service %q", gateway)) c.UI.Output(fmt.Sprintf("Successfully updated config entry for ingress service %q", gateway))
// Check for an existing intention. // Check for an existing intention.
ixnFinder := finder.Finder{Client: client} existing, _, err := client.Connect().IntentionGetExact(c.ingressGateway, c.service, nil)
existing, err := ixnFinder.Find(c.ingressGateway, c.service)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error looking up existing intention: %s", err)) c.UI.Error(fmt.Sprintf("Error looking up existing intention: %s", err))
return 1 return 1

View File

@ -31,6 +31,7 @@ func (c *cmd) init() {
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.NamespaceFlags())
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/intention/finder" "github.com/hashicorp/consul/command/intention"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -54,6 +54,7 @@ func (c *cmd) init() {
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.NamespaceFlags())
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
} }
@ -88,20 +89,21 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
// Create the finder in case we need it
find := &finder.Finder{Client: client}
// Go through and create each intention // Go through and create each intention
for _, ixn := range ixns { for _, ixn := range ixns {
// If replace is set to true, then perform an update operation. // If replace is set to true, then perform an update operation.
if c.flagReplace { if c.flagReplace {
oldIxn, err := find.Find(ixn.SourceString(), ixn.DestinationString()) oldIxn, _, err := client.Connect().IntentionGetExact(
intention.FormatSource(ixn),
intention.FormatDestination(ixn),
nil,
)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf( c.UI.Error(fmt.Sprintf(
"Error looking up intention for replacement with source %q "+ "Error looking up intention for replacement with source %q "+
"and destination %q: %s", "and destination %q: %s",
ixn.SourceString(), intention.FormatSource(ixn),
ixn.DestinationString(), intention.FormatDestination(ixn),
err)) err))
return 1 return 1
} }
@ -113,8 +115,8 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf( c.UI.Error(fmt.Sprintf(
"Error replacing intention with source %q "+ "Error replacing intention with source %q "+
"and destination %q: %s", "and destination %q: %s",
ixn.SourceString(), intention.FormatSource(ixn),
ixn.DestinationString(), intention.FormatDestination(ixn),
err)) err))
return 1 return 1
} }

View File

@ -31,6 +31,7 @@ func (c *cmd) init() {
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.NamespaceFlags())
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
} }
@ -47,8 +48,7 @@ func (c *cmd) Run(args []string) int {
} }
// Get the intention ID to load // Get the intention ID to load
f := &finder.Finder{Client: client} id, err := finder.IDFromArgs(client, c.flags.Args())
id, err := f.IDFromArgs(c.flags.Args())
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error: %s", err)) c.UI.Error(fmt.Sprintf("Error: %s", err))
return 1 return 1

View File

@ -2,38 +2,21 @@ package finder
import ( import (
"fmt" "fmt"
"strings"
"sync"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
) )
// Finder finds intentions by a src/dst exact match. There is currently // IDFromArgs returns the intention ID for the given CLI args. An error is returned
// no direct API to do this so this struct downloads all intentions and
// caches them once, and searches in-memory for this. For now this works since
// even with a very large number of intentions, the size of the data gzipped
// over HTTP will be relatively small.
//
// The Finder will only download the intentions one time. This struct is
// not expected to be used over a long period of time. Though it may be
// reused multile times, the intentions list is only downloaded once.
type Finder struct {
// Client is the API client to use for any requests.
Client *api.Client
lock sync.Mutex
ixns []*api.Intention // cached list of intentions
}
// ID returns the intention ID for the given CLI args. An error is returned
// if args is not 1 or 2 elements. // if args is not 1 or 2 elements.
func (f *Finder) IDFromArgs(args []string) (string, error) { func IDFromArgs(client *api.Client, args []string) (string, error) {
switch len(args) { switch len(args) {
case 1: case 1:
return args[0], nil return args[0], nil
case 2: case 2:
ixn, err := f.Find(args[0], args[1]) ixn, _, err := client.Connect().IntentionGetExact(
args[0], args[1], nil,
)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -49,44 +32,3 @@ func (f *Finder) IDFromArgs(args []string) (string, error) {
return "", fmt.Errorf("command requires exactly 1 or 2 arguments") return "", fmt.Errorf("command requires exactly 1 or 2 arguments")
} }
} }
// Find finds the intention that matches the given src and dst. This will
// return nil when the result is not found.
func (f *Finder) Find(src, dst string) (*api.Intention, error) {
src = StripDefaultNS(src)
dst = StripDefaultNS(dst)
f.lock.Lock()
defer f.lock.Unlock()
// If the list of ixns is nil, then we haven't fetched yet, so fetch
if f.ixns == nil {
ixns, _, err := f.Client.Connect().Intentions(nil)
if err != nil {
return nil, err
}
f.ixns = ixns
}
// Go through the intentions and find an exact match
for _, ixn := range f.ixns {
if ixn.SourceString() == src && ixn.DestinationString() == dst {
return ixn, nil
}
}
return nil, nil
}
// StripDefaultNS strips the default namespace from an argument. For now,
// the API and lookups strip this value from string output so we strip it.
func StripDefaultNS(v string) string {
if idx := strings.IndexByte(v, '/'); idx > 0 {
if v[:idx] == api.IntentionDefaultNamespace {
return v[idx+1:]
}
}
return v
}

View File

@ -8,10 +8,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestFinder(t *testing.T) { func TestIDFromArgs(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t, ``) a := agent.NewTestAgent(t, ``)
defer a.Shutdown() defer a.Shutdown()
client := a.Client() client := a.Client()
@ -20,30 +19,26 @@ func TestFinder(t *testing.T) {
var ids []string var ids []string
{ {
insert := [][]string{ insert := [][]string{
{"a", "b", "c", "d"}, {"a", "b"},
} }
for _, v := range insert { for _, v := range insert {
ixn := &api.Intention{ ixn := &api.Intention{
SourceNS: v[0], SourceName: v[0],
SourceName: v[1], DestinationName: v[1],
DestinationNS: v[2],
DestinationName: v[3],
Action: api.IntentionActionAllow, Action: api.IntentionActionAllow,
} }
id, _, err := client.Connect().IntentionCreate(ixn, nil) id, _, err := client.Connect().IntentionCreate(ixn, nil)
require.NoError(err) require.NoError(t, err)
ids = append(ids, id) ids = append(ids, id)
} }
} }
finder := &Finder{Client: client} id, err := IDFromArgs(client, []string{"a", "b"})
ixn, err := finder.Find("a/b", "c/d") require.NoError(t, err)
require.NoError(err) require.Equal(t, ids[0], id)
require.Equal(ids[0], ixn.ID)
ixn, err = finder.Find("a/c", "c/d") _, err = IDFromArgs(client, []string{"c", "d"})
require.NoError(err) require.Error(t, err)
require.Nil(ixn)
} }

View File

@ -0,0 +1,26 @@
package intention
import (
"github.com/hashicorp/consul/api"
)
// FormatSource returns the namespace/name format for the source. This is
// different from (*api.Intention).SourceString in that the default namespace
// is not omitted.
func FormatSource(i *api.Intention) string {
return partString(i.SourceNS, i.SourceName)
}
// FormatDestination returns the namespace/name format for the destination.
// This is different from (*api.Intention).DestinationString in that the
// default namespace is not omitted.
func FormatDestination(i *api.Intention) string {
return partString(i.DestinationNS, i.DestinationName)
}
func partString(ns, n string) string {
if ns == "" {
return n
}
return ns + "/" + n
}

View File

@ -34,6 +34,7 @@ func (c *cmd) init() {
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.NamespaceFlags())
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
} }
@ -50,8 +51,7 @@ func (c *cmd) Run(args []string) int {
} }
// Get the intention ID to load // Get the intention ID to load
f := &finder.Finder{Client: client} id, err := finder.IDFromArgs(client, c.flags.Args())
id, err := f.IDFromArgs(c.flags.Args())
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error: %s", err)) c.UI.Error(fmt.Sprintf("Error: %s", err))
return 1 return 1

View File

@ -40,6 +40,7 @@ func (c *cmd) init() {
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.NamespaceFlags())
c.help = flags.Usage(help, c.flags) c.help = flags.Usage(help, c.flags)
} }

View File

@ -50,22 +50,36 @@ The table below shows this endpoint's support for
For a `SourceType` of `consul` this is the name of a Consul service. The For a `SourceType` of `consul` this is the name of a Consul service. The
service doesn't need to be registered. service doesn't need to be registered.
- `SourceNS` `(string: "")` <EnterpriseAlert inline /> - The namespace for the
`SourceName` parameter.
- `DestinationName` `(string: <required>)` - The destination of the intention. - `DestinationName` `(string: <required>)` - The destination of the intention.
The intention destination is always a Consul service, unlike the source. The intention destination is always a Consul service, unlike the source.
The service doesn't need to be registered. The service doesn't need to be registered.
- `DestinationNS` `(string: "")` <EnterpriseAlert inline /> - The namespace for the
`DestinationName` parameter.
- `SourceType` `(string: <required>)` - The type for the `SourceName` value. - `SourceType` `(string: <required>)` - The type for the `SourceName` value.
This can be only "consul" today to represent a Consul service. This can be only "consul" today to represent a Consul service.
- `Action` `(string: <required>)` - This is one of "allow" or "deny" for - `Action` `(string: <required>)` - This is one of "allow" or "deny" for
the action that should be taken if this intention matches a request. the action that should be taken if this intention matches a request.
- `Description` `(string: nil)` - Description for the intention. This is not - `Description` `(string: "")` - Description for the intention. This is not
used for anything by Consul, but is presented in API responses to assist used for anything by Consul, but is presented in API responses to assist
tooling. tooling.
- `Meta` `(map<string|string>: nil)` - Specifies arbitrary KV metadata pairs. - `Meta` `(map<string|string>: nil)` - Specifies arbitrary KV metadata pairs.
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the default
namespace to use when `SourceNS` or `DestinationNS` are omitted or empty.
If not provided at all, the default namespace will be inherited from the
request's ACL token or will default to the `default` namespace.
This value may be provided by either the `ns` URL query parameter or in the
`X-Consul-Namespace` header.
Added in Consul 1.9.0.
### Sample Payload ### Sample Payload
```json ```json
@ -154,6 +168,79 @@ $ curl \
} }
``` ```
## Read Specific Intention by Name
This endpoint reads a specific intention by its unique source and destination.
| Method | Path | Produces |
| ------ | --------------------------- | ------------------ |
| `GET` | `/connect/intentions/exact` | `application/json` |
The table below shows this endpoint's support for
[blocking queries](/api/features/blocking),
[consistency modes](/api/features/consistency),
[agent caching](/api/features/caching), and
[required ACLs](/api#authentication).
| Blocking Queries | Consistency Modes | Agent Caching | ACL Required |
| ---------------- | ----------------- | ------------- | ----------------------------- |
| `YES` | `all` | `none` | `intentions:read`<sup>1</sup> |
<p>
<sup>1</sup> Intention ACL rules are specified as part of a `service` rule.
See{' '}
<a href="/docs/connect/intentions#intention-management-permissions">
Intention Management Permissions
</a>{' '}
for more details.
</p>
### Parameters
- `source` `(string: <required>)` - Specifies the source service. This
is specified as part of the URL.
This can take [several forms](/docs/commands/intention#source-and-destination-naming).
- `destination` `(string: <required>)` - Specifies the destination service. This
is specified as part of the URL.
This can take [several forms](/docs/commands/intention#source-and-destination-naming).
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the default
namespace to use when `source` or `destination` parameters lack namespaces.
If not provided at all, the default namespace will be inherited from the
request's ACL token or will default to the `default` namespace.
This value may be provided by either the `ns` URL query parameter or in the
`X-Consul-Namespace` header.
Added in Consul 1.9.0.
### Sample Request
```shell-session
$ curl \
http://127.0.0.1:8500/v1/connect/intentions/exact?source=web&destination=db
```
### Sample Response
```json
{
"ID": "e9ebc19f-d481-42b1-4871-4d298d3acd5c",
"Description": "",
"SourceNS": "default",
"SourceName": "web",
"DestinationNS": "default",
"DestinationName": "db",
"SourceType": "consul",
"Action": "allow",
"Meta": {},
"Precedence": 9,
"CreatedAt": "2018-05-21T16:41:27.977155457Z",
"UpdatedAt": "2018-05-21T16:41:27.977157724Z",
"CreateIndex": 11,
"ModifyIndex": 11
}
```
## List Intentions ## List Intentions
This endpoint lists all intentions. This endpoint lists all intentions.
@ -186,6 +273,15 @@ The table below shows this endpoint's support for
- `filter` `(string: "")` - Specifies the expression used to filter the - `filter` `(string: "")` - Specifies the expression used to filter the
queries results prior to returning the data. queries results prior to returning the data.
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the
namespace to list intentions from.
The `*` wildcard may be used to list intentions from all namespaces.
If not provided at all, the default namespace will be inherited from the
request's ACL token or will default to the `default` namespace.
This value may be provided by either the `ns` URL query parameter or in the
`X-Consul-Namespace` header.
Added in Consul 1.9.0.
### Sample Request ### Sample Request
```shell-session ```shell-session
@ -366,9 +462,19 @@ The table below shows this endpoint's support for
- `source` `(string: <required>)` - Specifies the source service. This - `source` `(string: <required>)` - Specifies the source service. This
is specified as part of the URL. is specified as part of the URL.
This can take [several forms](/docs/commands/intention#source-and-destination-naming).
- `destination` `(string: <required>)` - Specifies the destination service. This - `destination` `(string: <required>)` - Specifies the destination service. This
is specified as part of the URL. is specified as part of the URL.
This can take [several forms](/docs/commands/intention#source-and-destination-naming).
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the default
namespace to use when `source` or `destination` parameters lack namespaces.
If not provided at all, the default namespace will be inherited from the
request's ACL token or will default to the `default` namespace.
This value may be provided by either the `ns` URL query parameter or in the
`X-Consul-Namespace` header.
Added in Consul 1.9.0.
### Sample Request ### Sample Request
@ -422,6 +528,15 @@ The table below shows this endpoint's support for
- `name` `(string: <required>)` - Specifies a name to match. This parameter - `name` `(string: <required>)` - Specifies a name to match. This parameter
can be repeated for batching multiple matches. can be repeated for batching multiple matches.
This can take [several forms](/docs/commands/intention#source-and-destination-naming).
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the default
namespace to use when `name` parameter lacks namespaces.
If not provided at all, the default namespace will be inherited from the
request's ACL token or will default to the `default` namespace.
This value may be provided by either the `ns` URL query parameter or in the
`X-Consul-Namespace` header.
Added in Consul 1.9.0.
### Sample Request ### Sample Request

View File

@ -22,10 +22,16 @@ intention read permissions and don't evaluate the result.
Usage: `consul intention check [options] SRC DST` Usage: `consul intention check [options] SRC DST`
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
#### API Options #### API Options
@include 'http_api_options_client.mdx' @include 'http_api_options_client.mdx'
#### Enterprise Options
@include 'http_api_namespace_options.mdx'
## Examples ## Examples
```shell-session ```shell-session

View File

@ -15,10 +15,16 @@ The `intention create` command creates or updates an intention.
Usage: `consul intention create [options] SRC DST` Usage: `consul intention create [options] SRC DST`
Usage: `consul intention create [options] -f FILE...` Usage: `consul intention create [options] -f FILE...`
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
#### API Options #### API Options
@include 'http_api_options_client.mdx' @include 'http_api_options_client.mdx'
#### Enterprise Options
@include 'http_api_namespace_options.mdx'
#### Intention Create Options #### Intention Create Options
- `-allow` - Set the action to "allow" for intentions. This is the default. - `-allow` - Set the action to "allow" for intentions. This is the default.

View File

@ -17,10 +17,16 @@ Usage:
- `consul intention delete [options] SRC DST` - `consul intention delete [options] SRC DST`
- `consul intention delete [options] ID` - `consul intention delete [options] ID`
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
#### API Options #### API Options
@include 'http_api_options_client.mdx' @include 'http_api_options_client.mdx'
#### Enterprise Options
@include 'http_api_namespace_options.mdx'
## Examples ## Examples
Delete an intention from "web" to "db" with any action: Delete an intention from "web" to "db" with any action:

View File

@ -17,10 +17,16 @@ Usage:
- `consul intention get [options] SRC DST` - `consul intention get [options] SRC DST`
- `consul intention get [options] ID` - `consul intention get [options] ID`
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
#### API Options #### API Options
@include 'http_api_options_client.mdx' @include 'http_api_options_client.mdx'
#### Enterprise Options
@include 'http_api_namespace_options.mdx'
## Examples ## Examples
```shell-session ```shell-session

View File

@ -64,3 +64,16 @@ Find all intentions for communicating to the "db" service:
```shell-session ```shell-session
$ consul intention match db $ consul intention match db
``` ```
## Source and Destination Naming
Intention commands commonly take positional arguments referred to as `SRC` and
`DST` in the command documentation. These can take several forms:
| Format | Meaning |
| ----------------------- | -----------------------------------------------------------------------|
| `<service>` | the named service in the current namespace |
| `*` | any service in the current namespace |
| `<namespace>/<service>` | <EnterpriseAlert inline /> the named service in a specific namespace |
| `<namespace>/*` | <EnterpriseAlert inline /> any service in the specified namespace |
| `*/*` | <EnterpriseAlert inline /> any service in any namespace |

View File

@ -19,10 +19,16 @@ check whether a connection would be authorized between any two services.
Usage: `consul intention match [options] SRC_OR_DST` Usage: `consul intention match [options] SRC_OR_DST`
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
#### API Options #### API Options
@include 'http_api_options_client.mdx' @include 'http_api_options_client.mdx'
#### Enterprise Options
@include 'http_api_namespace_options.mdx'
#### Intention Match Options #### Intention Match Options
- `-destination` - Match by destination. - `-destination` - Match by destination.