mirror of https://github.com/status-im/consul.git
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:
parent
10d6e9c458
commit
462f0f37ed
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue