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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/agent/pool"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -59,6 +61,18 @@ func (s *Server) validateEnterpriseRequest(entMeta *structs.EnterpriseMeta, writ
|
|||
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) {
|
||||
// do nothing
|
||||
}
|
||||
|
|
|
@ -576,7 +576,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
|
|||
require.Equal(t, autopilotConf, restoredConf)
|
||||
|
||||
// Verify intentions are restored.
|
||||
_, ixns, err := fsm2.state.Intentions(nil)
|
||||
_, ixns, err := fsm2.state.Intentions(nil, structs.WildcardEnterpriseMeta())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ixns, 1)
|
||||
require.Equal(t, ixn, ixns[0])
|
||||
|
|
|
@ -76,6 +76,10 @@ func (s *Intention) prepareApplyCreate(ident structs.ACLIdentity, authz acl.Auth
|
|||
|
||||
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
|
||||
// send an ID in that case.
|
||||
// Set the precedence
|
||||
|
@ -135,11 +139,15 @@ func (s *Intention) prepareApplyUpdate(ident structs.ACLIdentity, authz acl.Auth
|
|||
|
||||
args.Intention.DefaultNamespaces(entMeta)
|
||||
|
||||
// Validate. We do not validate on delete since it is valid to only
|
||||
// send an ID in that case.
|
||||
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the precedence
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -249,11 +257,44 @@ func (s *Intention) Get(
|
|||
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(
|
||||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -296,10 +337,19 @@ func (s *Intention) List(
|
|||
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(
|
||||
&args.QueryOptions, &reply.QueryMeta,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -334,12 +384,24 @@ func (s *Intention) Match(
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
// 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.
|
||||
for _, entry := range args.Match.Entries {
|
||||
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)
|
||||
// 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)
|
||||
|
@ -396,6 +458,28 @@ func (s *Intention) Check(
|
|||
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
|
||||
var uri connect.CertURI
|
||||
switch query.SourceType {
|
||||
|
@ -409,12 +493,6 @@ func (s *Intention) Check(
|
|||
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
|
||||
// NOT IntentionRead because the Check API only returns pass/fail and
|
||||
// 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 {
|
||||
var authzContext acl.AuthorizerContext
|
||||
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)
|
||||
// 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)
|
||||
|
@ -470,14 +548,14 @@ func (s *Intention) Check(
|
|||
// NOTE(mitchellh): This is the same behavior as the agent authorize
|
||||
// endpoint. If this behavior is incorrect, we should also change it there
|
||||
// which is much more important.
|
||||
rule, err = s.srv.ResolveToken("")
|
||||
authz, err = s.srv.ResolveToken("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Allowed = true
|
||||
if rule != nil {
|
||||
reply.Allowed = rule.IntentionDefaultAllow(nil) == acl.Allow
|
||||
if authz != nil {
|
||||
reply.Allowed = authz.IntentionDefaultAllow(nil) == acl.Allow
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -500,3 +578,13 @@ func (s *Intention) aclAccessorID(secretID string) string {
|
|||
}
|
||||
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/agent/structs"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -17,14 +15,14 @@ import (
|
|||
func TestIntentionApply_new(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -46,8 +44,8 @@ func TestIntentionApply_new(t *testing.T) {
|
|||
now := time.Now()
|
||||
|
||||
// Create
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
assert.NotEmpty(reply)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.NotEmpty(reply)
|
||||
|
||||
// Read
|
||||
ixn.Intention.ID = reply
|
||||
|
@ -57,19 +55,19 @@ func TestIntentionApply_new(t *testing.T) {
|
|||
IntentionID: ixn.Intention.ID,
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
require.Len(resp.Intentions, 1)
|
||||
actual := resp.Intentions[0]
|
||||
assert.Equal(resp.Index, actual.ModifyIndex)
|
||||
assert.WithinDuration(now, actual.CreatedAt, 5*time.Second)
|
||||
assert.WithinDuration(now, actual.UpdatedAt, 5*time.Second)
|
||||
require.Equal(resp.Index, actual.ModifyIndex)
|
||||
require.WithinDuration(now, actual.CreatedAt, 5*time.Second)
|
||||
require.WithinDuration(now, actual.UpdatedAt, 5*time.Second)
|
||||
|
||||
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||
actual.CreatedAt = ixn.Intention.CreatedAt
|
||||
actual.UpdatedAt = ixn.Intention.UpdatedAt
|
||||
actual.Hash = ixn.Intention.Hash
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -101,8 +99,8 @@ func TestIntentionApply_defaultSourceType(t *testing.T) {
|
|||
var reply string
|
||||
|
||||
// Create
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
assert.NotEmpty(reply)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.NotEmpty(reply)
|
||||
|
||||
// Read
|
||||
ixn.Intention.ID = reply
|
||||
|
@ -112,10 +110,10 @@ func TestIntentionApply_defaultSourceType(t *testing.T) {
|
|||
IntentionID: ixn.Intention.ID,
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
require.Len(resp.Intentions, 1)
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -145,22 +143,22 @@ func TestIntentionApply_createWithID(t *testing.T) {
|
|||
|
||||
// Create
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
||||
assert.NotNil(err)
|
||||
assert.Contains(err, "ID must be empty")
|
||||
require.NotNil(err)
|
||||
require.Contains(err, "ID must be empty")
|
||||
}
|
||||
|
||||
// Test basic updating
|
||||
func TestIntentionApply_updateGood(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -179,8 +177,8 @@ func TestIntentionApply_updateGood(t *testing.T) {
|
|||
var reply string
|
||||
|
||||
// Create
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
assert.NotEmpty(reply)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.NotEmpty(reply)
|
||||
|
||||
// Read CreatedAt
|
||||
var createdAt time.Time
|
||||
|
@ -191,8 +189,8 @@ func TestIntentionApply_updateGood(t *testing.T) {
|
|||
IntentionID: ixn.Intention.ID,
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
require.Len(resp.Intentions, 1)
|
||||
actual := resp.Intentions[0]
|
||||
createdAt = actual.CreatedAt
|
||||
}
|
||||
|
@ -204,7 +202,7 @@ func TestIntentionApply_updateGood(t *testing.T) {
|
|||
ixn.Op = structs.IntentionOpUpdate
|
||||
ixn.Intention.ID = reply
|
||||
ixn.Intention.SourceName = "*"
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
|
||||
// Read
|
||||
ixn.Intention.ID = reply
|
||||
|
@ -214,18 +212,18 @@ func TestIntentionApply_updateGood(t *testing.T) {
|
|||
IntentionID: ixn.Intention.ID,
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
require.Len(resp.Intentions, 1)
|
||||
actual := resp.Intentions[0]
|
||||
assert.Equal(createdAt, actual.CreatedAt)
|
||||
assert.WithinDuration(time.Now(), actual.UpdatedAt, 5*time.Second)
|
||||
require.Equal(createdAt, actual.CreatedAt)
|
||||
require.WithinDuration(time.Now(), actual.UpdatedAt, 5*time.Second)
|
||||
|
||||
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||
actual.CreatedAt = ixn.Intention.CreatedAt
|
||||
actual.UpdatedAt = ixn.Intention.UpdatedAt
|
||||
actual.Hash = ixn.Intention.Hash
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -255,31 +253,29 @@ func TestIntentionApply_updateNonExist(t *testing.T) {
|
|||
|
||||
// Create
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
||||
assert.NotNil(err)
|
||||
assert.Contains(err, "Cannot modify non-existent intention")
|
||||
require.NotNil(err)
|
||||
require.Contains(err, "Cannot modify non-existent intention")
|
||||
}
|
||||
|
||||
// Test basic deleting
|
||||
func TestIntentionApply_deleteGood(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: &structs.Intention{
|
||||
SourceNS: "test",
|
||||
SourceName: "test",
|
||||
DestinationNS: "test",
|
||||
DestinationName: "test",
|
||||
Action: structs.IntentionActionAllow,
|
||||
},
|
||||
|
@ -287,13 +283,13 @@ func TestIntentionApply_deleteGood(t *testing.T) {
|
|||
var reply string
|
||||
|
||||
// Create
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
assert.NotEmpty(reply)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.NotEmpty(reply)
|
||||
|
||||
// Delete
|
||||
ixn.Op = structs.IntentionOpDelete
|
||||
ixn.Intention.ID = reply
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
|
||||
// Read
|
||||
ixn.Intention.ID = reply
|
||||
|
@ -304,8 +300,8 @@ func TestIntentionApply_deleteGood(t *testing.T) {
|
|||
}
|
||||
var resp structs.IndexedIntentions
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
||||
assert.NotNil(err)
|
||||
assert.Contains(err, ErrIntentionNotFound.Error())
|
||||
require.NotNil(err)
|
||||
require.Contains(err, ErrIntentionNotFound.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,7 +309,7 @@ func TestIntentionApply_deleteGood(t *testing.T) {
|
|||
func TestIntentionApply_aclDeny(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -325,7 +321,7 @@ func TestIntentionApply_aclDeny(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with write permissions
|
||||
var token string
|
||||
|
@ -346,7 +342,7 @@ service "foo" {
|
|||
},
|
||||
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
|
||||
|
@ -360,11 +356,11 @@ service "foo" {
|
|||
// Create without a token should error since default deny
|
||||
var reply string
|
||||
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.
|
||||
ixn.WriteRequest.Token = token
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
|
||||
// Read
|
||||
ixn.Intention.ID = reply
|
||||
|
@ -375,17 +371,17 @@ service "foo" {
|
|||
QueryOptions: structs.QueryOptions{Token: "root"},
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
require.Len(resp.Intentions, 1)
|
||||
actual := resp.Intentions[0]
|
||||
assert.Equal(resp.Index, actual.ModifyIndex)
|
||||
require.Equal(resp.Index, actual.ModifyIndex)
|
||||
|
||||
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||
actual.CreatedAt = ixn.Intention.CreatedAt
|
||||
actual.UpdatedAt = ixn.Intention.UpdatedAt
|
||||
actual.Hash = ixn.Intention.Hash
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -719,7 +715,7 @@ func TestIntentionApply_aclDelete(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with write permissions
|
||||
var token string
|
||||
|
@ -740,7 +736,7 @@ service "foo" {
|
|||
},
|
||||
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
|
||||
|
@ -754,18 +750,18 @@ service "foo" {
|
|||
|
||||
// Create
|
||||
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.
|
||||
ixn.Op = structs.IntentionOpDelete
|
||||
ixn.Intention.ID = reply
|
||||
ixn.WriteRequest.Token = ""
|
||||
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.
|
||||
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
|
||||
{
|
||||
|
@ -775,8 +771,8 @@ service "foo" {
|
|||
}
|
||||
var resp structs.IndexedIntentions
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
||||
assert.NotNil(err)
|
||||
assert.Contains(err.Error(), ErrIntentionNotFound.Error())
|
||||
require.NotNil(err)
|
||||
require.Contains(err.Error(), ErrIntentionNotFound.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -784,7 +780,7 @@ service "foo" {
|
|||
func TestIntentionApply_aclUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -796,7 +792,7 @@ func TestIntentionApply_aclUpdate(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with write permissions
|
||||
var token string
|
||||
|
@ -817,7 +813,7 @@ service "foo" {
|
|||
},
|
||||
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
|
||||
|
@ -831,25 +827,25 @@ service "foo" {
|
|||
|
||||
// Create
|
||||
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.
|
||||
ixn.Op = structs.IntentionOpUpdate
|
||||
ixn.Intention.ID = reply
|
||||
ixn.WriteRequest.Token = ""
|
||||
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.
|
||||
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
|
||||
func TestIntentionApply_aclManagement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -861,7 +857,7 @@ func TestIntentionApply_aclManagement(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -874,23 +870,23 @@ func TestIntentionApply_aclManagement(t *testing.T) {
|
|||
|
||||
// Create
|
||||
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
|
||||
|
||||
// Update
|
||||
ixn.Op = structs.IntentionOpUpdate
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||
|
||||
// Delete
|
||||
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
|
||||
func TestIntentionApply_aclUpdateChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -902,7 +898,7 @@ func TestIntentionApply_aclUpdateChange(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with write permissions
|
||||
var token string
|
||||
|
@ -923,7 +919,7 @@ service "foo" {
|
|||
},
|
||||
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
|
||||
|
@ -937,7 +933,7 @@ service "foo" {
|
|||
|
||||
// Create
|
||||
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.
|
||||
ixn.Op = structs.IntentionOpUpdate
|
||||
|
@ -945,14 +941,13 @@ service "foo" {
|
|||
ixn.Intention.DestinationName = "foo"
|
||||
ixn.WriteRequest.Token = token
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
||||
assert.True(acl.IsErrPermissionDenied(err))
|
||||
require.True(acl.IsErrPermissionDenied(err))
|
||||
}
|
||||
|
||||
// Test reading with ACLs
|
||||
func TestIntentionGet_acl(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -964,29 +959,15 @@ func TestIntentionGet_acl(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with service write permissions. This will grant
|
||||
// intentions read.
|
||||
var token string
|
||||
{
|
||||
var rules = `
|
||||
service "foo" {
|
||||
policy = "write"
|
||||
}`
|
||||
|
||||
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))
|
||||
}
|
||||
// intentions read on either end of an intention.
|
||||
token, err := upsertTestTokenWithPolicyRules(codec, "root", "dc1", `
|
||||
service "foobar" {
|
||||
policy = "write"
|
||||
}`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
|
@ -999,11 +980,10 @@ service "foo" {
|
|||
|
||||
// Create
|
||||
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
|
||||
|
||||
// Read without token should be error
|
||||
{
|
||||
t.Run("Read by ID without token should be error", func(t *testing.T) {
|
||||
req := &structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
IntentionID: ixn.Intention.ID,
|
||||
|
@ -1011,35 +991,68 @@ service "foo" {
|
|||
|
||||
var resp structs.IndexedIntentions
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
||||
assert.True(acl.IsErrPermissionDenied(err))
|
||||
assert.Len(resp.Intentions, 0)
|
||||
}
|
||||
require.True(t, acl.IsErrPermissionDenied(err))
|
||||
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{
|
||||
Datacenter: "dc1",
|
||||
IntentionID: ixn.Intention.ID,
|
||||
QueryOptions: structs.QueryOptions{Token: token},
|
||||
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
||||
}
|
||||
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
assert.Len(resp.Intentions, 1)
|
||||
}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Test with no intentions inserted yet
|
||||
{
|
||||
|
@ -1047,9 +1060,9 @@ func TestIntentionList(t *testing.T) {
|
|||
Datacenter: "dc1",
|
||||
}
|
||||
var resp structs.IndexedIntentions
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||
assert.NotNil(resp.Intentions)
|
||||
assert.Len(resp.Intentions, 0)
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||
require.NotNil(resp.Intentions)
|
||||
require.Len(resp.Intentions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1063,7 +1076,7 @@ func TestIntentionList_acl(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
waitForNewACLs(t, s1)
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create some records
|
||||
{
|
||||
insert := [][]string{
|
||||
{"foo", "*", "foo", "*"},
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "baz"}, // shouldn't match
|
||||
{"foo", "*", "bar", "bar"}, // shouldn't match
|
||||
{"foo", "*", "bar", "*"}, // shouldn't match
|
||||
{"foo", "*", "*", "*"},
|
||||
{"bar", "*", "foo", "bar"}, // duplicate destination different source
|
||||
{"default", "*", "default", "*"},
|
||||
{"default", "*", "default", "bar"},
|
||||
{"default", "*", "default", "baz"}, // shouldn't match
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
|
@ -1174,7 +1182,7 @@ func TestIntentionMatch_good(t *testing.T) {
|
|||
|
||||
// Create
|
||||
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{
|
||||
Type: structs.IntentionMatchDestination,
|
||||
Entries: []structs.IntentionMatchEntry{
|
||||
{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
{Name: "bar"},
|
||||
},
|
||||
},
|
||||
}
|
||||
var resp structs.IndexedIntentionMatches
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
|
||||
assert.Len(resp.Matches, 1)
|
||||
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
|
||||
require.Len(t, resp.Matches, 1)
|
||||
|
||||
expected := [][]string{
|
||||
{"bar", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "*"},
|
||||
{"foo", "*", "*", "*"},
|
||||
{"default", "*", "default", "bar"},
|
||||
{"default", "*", "default", "*"},
|
||||
}
|
||||
var actual [][]string
|
||||
for _, ixn := range resp.Matches[0] {
|
||||
|
@ -1210,7 +1213,7 @@ func TestIntentionMatch_good(t *testing.T) {
|
|||
ixn.DestinationName,
|
||||
})
|
||||
}
|
||||
assert.Equal(expected, actual)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
// Test matching with ACLs
|
||||
|
@ -1299,36 +1302,32 @@ func TestIntentionMatch_acl(t *testing.T) {
|
|||
func TestIntentionCheck_defaultNoACL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Test
|
||||
req := &structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Check: &structs.IntentionQueryCheck{
|
||||
SourceNS: "foo",
|
||||
SourceName: "bar",
|
||||
DestinationNS: "foo",
|
||||
DestinationName: "qux",
|
||||
SourceType: structs.IntentionSourceConsul,
|
||||
},
|
||||
}
|
||||
var resp structs.IntentionQueryCheckResponse
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.True(resp.Allowed)
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.True(t, resp.Allowed)
|
||||
}
|
||||
|
||||
// Test the Check method defaults to deny with allowlist ACLs.
|
||||
func TestIntentionCheck_defaultACLDeny(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -1340,30 +1339,27 @@ func TestIntentionCheck_defaultACLDeny(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Check
|
||||
req := &structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Check: &structs.IntentionQueryCheck{
|
||||
SourceNS: "foo",
|
||||
SourceName: "bar",
|
||||
DestinationNS: "foo",
|
||||
DestinationName: "qux",
|
||||
SourceType: structs.IntentionSourceConsul,
|
||||
},
|
||||
}
|
||||
req.Token = "root"
|
||||
var resp structs.IntentionQueryCheckResponse
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.False(resp.Allowed)
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.False(t, resp.Allowed)
|
||||
}
|
||||
|
||||
// Test the Check method defaults to deny with denylist ACLs.
|
||||
func TestIntentionCheck_defaultACLAllow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -1375,30 +1371,27 @@ func TestIntentionCheck_defaultACLAllow(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Check
|
||||
req := &structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Check: &structs.IntentionQueryCheck{
|
||||
SourceNS: "foo",
|
||||
SourceName: "bar",
|
||||
DestinationNS: "foo",
|
||||
DestinationName: "qux",
|
||||
SourceType: structs.IntentionSourceConsul,
|
||||
},
|
||||
}
|
||||
req.Token = "root"
|
||||
var resp structs.IntentionQueryCheckResponse
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.True(resp.Allowed)
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
||||
require.True(t, resp.Allowed)
|
||||
}
|
||||
|
||||
// Test the Check method requires service:read permission.
|
||||
func TestIntentionCheck_aclDeny(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
|
@ -1410,7 +1403,7 @@ func TestIntentionCheck_aclDeny(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Create an ACL with service read permissions. This will grant permission.
|
||||
var token string
|
||||
|
@ -1430,16 +1423,14 @@ service "bar" {
|
|||
},
|
||||
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
|
||||
req := &structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Check: &structs.IntentionQueryCheck{
|
||||
SourceNS: "foo",
|
||||
SourceName: "qux",
|
||||
DestinationNS: "foo",
|
||||
DestinationName: "baz",
|
||||
SourceType: structs.IntentionSourceConsul,
|
||||
},
|
||||
|
@ -1447,7 +1438,7 @@ service "bar" {
|
|||
req.Token = token
|
||||
var resp structs.IntentionQueryCheckResponse
|
||||
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.
|
||||
|
|
|
@ -559,6 +559,7 @@ func (s *Server) startConnectLeader() {
|
|||
s.leaderRoutineManager.Start(secondaryCARootWatchRoutineName, s.secondaryCARootWatch)
|
||||
s.leaderRoutineManager.Start(intentionReplicationRoutineName, s.replicateIntentions)
|
||||
s.leaderRoutineManager.Start(secondaryCertRenewWatchRoutineName, s.secondaryIntermediateCertRenewalWatch)
|
||||
s.startConnectLeaderEnterprise()
|
||||
}
|
||||
|
||||
s.leaderRoutineManager.Start(caRootPruningRoutineName, s.runCARootPruning)
|
||||
|
@ -569,6 +570,7 @@ func (s *Server) stopConnectLeader() {
|
|||
s.leaderRoutineManager.Stop(secondaryCARootWatchRoutineName)
|
||||
s.leaderRoutineManager.Stop(intentionReplicationRoutineName)
|
||||
s.leaderRoutineManager.Stop(caRootPruningRoutineName)
|
||||
s.stopConnectLeaderEnterprise()
|
||||
}
|
||||
|
||||
func (s *Server) runCARootPruning(ctx context.Context) error {
|
||||
|
@ -789,7 +791,7 @@ func (s *Server) replicateIntentions(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
_, local, err := s.fsm.State().Intentions(nil)
|
||||
_, local, err := s.fsm.State().Intentions(nil, s.replicationEnterpriseMeta())
|
||||
if err != nil {
|
||||
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.
|
||||
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)
|
||||
defer tx.Abort()
|
||||
|
||||
|
@ -136,11 +136,11 @@ func (s *Store) Intentions(ws memdb.WatchSet) (uint64, structs.Intentions, error
|
|||
idx = 1
|
||||
}
|
||||
|
||||
// Get all intentions
|
||||
iter, err := tx.Get(intentionsTableName, "id")
|
||||
iter, err := s.intentionListTxn(tx, entMeta)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
var results structs.Intentions
|
||||
|
@ -250,6 +250,38 @@ func (s *Store) IntentionGet(ws memdb.WatchSet, id string) (uint64, *structs.Int
|
|||
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.
|
||||
func (s *Store) IntentionDelete(idx uint64, id string) error {
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
s := testStateStore(t)
|
||||
|
||||
entMeta := structs.WildcardEnterpriseMeta()
|
||||
|
||||
// Querying with no results returns nil.
|
||||
ws := memdb.NewWatchSet()
|
||||
idx, res, err := s.Intentions(ws)
|
||||
assert.NoError(err)
|
||||
assert.Nil(res)
|
||||
assert.Equal(uint64(1), idx)
|
||||
idx, res, err := s.Intentions(ws, entMeta)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, res)
|
||||
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
|
||||
ixns := structs.Intentions{
|
||||
&structs.Intention{
|
||||
ID: testUUID(),
|
||||
Meta: map[string]string{},
|
||||
},
|
||||
&structs.Intention{
|
||||
ID: testUUID(),
|
||||
Meta: map[string]string{},
|
||||
},
|
||||
testIntention("default", "foo", "default", "bar"),
|
||||
testIntention("default", "foo", "default", "*"),
|
||||
testIntention("*", "*", "default", "*"),
|
||||
testIntention("default", "*", "*", "*"),
|
||||
testIntention("*", "*", "*", "*"),
|
||||
}
|
||||
|
||||
// Force deterministic sort order
|
||||
ixns[0].ID = "a" + ixns[0].ID[1:]
|
||||
ixns[1].ID = "b" + ixns[1].ID[1:]
|
||||
|
||||
// Create
|
||||
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.
|
||||
expected := structs.Intentions{
|
||||
&structs.Intention{
|
||||
ID: ixns[0].ID,
|
||||
Meta: map[string]string{},
|
||||
RaftIndex: structs.RaftIndex{
|
||||
CreateIndex: 1,
|
||||
ModifyIndex: 1,
|
||||
},
|
||||
},
|
||||
&structs.Intention{
|
||||
ID: ixns[1].ID,
|
||||
Meta: map[string]string{},
|
||||
RaftIndex: structs.RaftIndex{
|
||||
CreateIndex: 2,
|
||||
ModifyIndex: 2,
|
||||
},
|
||||
},
|
||||
cmpIntention(testIntention("default", "foo", "default", "bar"), ixns[0].ID, 1),
|
||||
cmpIntention(testIntention("default", "foo", "default", "*"), ixns[1].ID, 2),
|
||||
cmpIntention(testIntention("*", "*", "default", "*"), ixns[2].ID, 3),
|
||||
cmpIntention(testIntention("default", "*", "*", "*"), ixns[3].ID, 4),
|
||||
cmpIntention(testIntention("*", "*", "*", "*"), ixns[4].ID, 5),
|
||||
}
|
||||
for i := range expected {
|
||||
expected[i].UpdatePrecedence() // to match what is returned...
|
||||
}
|
||||
idx, actual, err := s.Intentions(nil)
|
||||
assert.NoError(err)
|
||||
assert.Equal(idx, uint64(2))
|
||||
assert.Equal(expected, actual)
|
||||
|
||||
idx, actual, err := s.Intentions(nil, entMeta)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, idx, uint64(5))
|
||||
require.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
// 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
|
||||
// to rearrange the expected slice some.
|
||||
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.Equal(idx, uint64(6))
|
||||
assert.Equal(expected, actual)
|
||||
|
|
|
@ -77,7 +77,7 @@ func TestStateStore_Txn_Intention(t *testing.T) {
|
|||
require.Equal(t, expected, results)
|
||||
|
||||
// Pull the resulting state store contents.
|
||||
idx, actual, err := s.Intentions(nil)
|
||||
idx, actual, err := s.Intentions(nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(3), idx, "wrong index")
|
||||
|
||||
|
|
|
@ -20,6 +20,18 @@ func (s *HTTPServer) parseEntMeta(req *http.Request, entMeta *structs.Enterprise
|
|||
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 {
|
||||
return s.parseEntMeta(req, nil)
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ func init() {
|
|||
registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionEndpoint)
|
||||
registerEndpoint("/v1/connect/intentions/match", []string{"GET"}, (*HTTPServer).IntentionMatch)
|
||||
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/coordinate/datacenters", []string{"GET"}, (*HTTPServer).CoordinateDatacenters)
|
||||
registerEndpoint("/v1/coordinate/nodes", []string{"GET"}, (*HTTPServer).CoordinateNodes)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
// /v1/connection/intentions
|
||||
// /v1/connect/intentions
|
||||
func (s *HTTPServer) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
|
@ -32,6 +32,10 @@ func (s *HTTPServer) IntentionList(resp http.ResponseWriter, req *http.Request)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reply structs.IndexedIntentions
|
||||
defer setMeta(resp, &reply.QueryMeta)
|
||||
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) {
|
||||
// Method is tested in IntentionEndpoint
|
||||
|
||||
var entMeta structs.EnterpriseMeta
|
||||
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := structs.IntentionRequest{
|
||||
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)
|
||||
}
|
||||
|
||||
args.Intention.FillNonDefaultNamespaces(&entMeta)
|
||||
|
||||
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reply string
|
||||
if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
|
||||
return nil, err
|
||||
|
@ -62,6 +77,16 @@ func (s *HTTPServer) IntentionCreate(resp http.ResponseWriter, req *http.Request
|
|||
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
|
||||
func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Prepare args
|
||||
|
@ -70,6 +95,11 @@ func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
var entMeta structs.EnterpriseMeta
|
||||
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
|
||||
// Extract the "by" query parameter
|
||||
|
@ -94,7 +124,7 @@ func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request)
|
|||
// order of the returned responses.
|
||||
args.Match.Entries = make([]structs.IntentionMatchEntry, len(names))
|
||||
for i, n := range names {
|
||||
entry, err := parseIntentionMatchEntry(n)
|
||||
entry, err := parseIntentionMatchEntry(n, &entMeta)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
var entMeta structs.EnterpriseMeta
|
||||
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
|
||||
// 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
|
||||
args.Check.SourceName = source[0]
|
||||
if args.Check.SourceType == structs.IntentionSourceConsul {
|
||||
entry, err := parseIntentionMatchEntry(source[0])
|
||||
entry, err := parseIntentionMatchEntry(source[0], &entMeta)
|
||||
if err != nil {
|
||||
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
|
||||
entry, err := parseIntentionMatchEntry(destination[0])
|
||||
entry, err := parseIntentionMatchEntry(destination[0], &entMeta)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
// Method is tested in IntentionEndpoint
|
||||
|
||||
var entMeta structs.EnterpriseMeta
|
||||
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := structs.IntentionRequest{
|
||||
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)}
|
||||
}
|
||||
|
||||
args.Intention.FillNonDefaultNamespaces(&entMeta)
|
||||
|
||||
// Use the ID from the URL
|
||||
args.Intention.ID = id
|
||||
|
||||
|
@ -284,9 +400,9 @@ type intentionCreateResponse struct{ ID string }
|
|||
|
||||
// parseIntentionMatchEntry parses the query parameter for an intention
|
||||
// match query entry.
|
||||
func parseIntentionMatchEntry(input string) (structs.IntentionMatchEntry, error) {
|
||||
func parseIntentionMatchEntry(input string, entMeta *structs.EnterpriseMeta) (structs.IntentionMatchEntry, error) {
|
||||
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
|
||||
// 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) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
|
||||
// Create some intentions
|
||||
{
|
||||
insert := [][]string{
|
||||
{"foo", "*", "foo", "*"},
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "baz"}, // shouldn't match
|
||||
{"foo", "*", "bar", "bar"}, // shouldn't match
|
||||
{"foo", "*", "bar", "*"}, // shouldn't match
|
||||
{"foo", "*", "*", "*"},
|
||||
{"bar", "*", "foo", "bar"}, // duplicate destination different source
|
||||
{"default", "*", "default", "*"},
|
||||
{"default", "*", "default", "bar"},
|
||||
{"default", "*", "default", "baz"}, // shouldn't match
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
|
@ -100,28 +95,26 @@ func TestIntentionsMatch_basic(t *testing.T) {
|
|||
|
||||
// Create
|
||||
var reply string
|
||||
assert.Nil(a.RPC("Intention.Apply", &ixn, &reply))
|
||||
require.Nil(t, a.RPC("Intention.Apply", &ixn, &reply))
|
||||
}
|
||||
}
|
||||
|
||||
// Request
|
||||
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()
|
||||
obj, err := a.srv.IntentionMatch(resp, req)
|
||||
assert.Nil(err)
|
||||
require.Nil(t, err)
|
||||
|
||||
value := obj.(map[string]structs.Intentions)
|
||||
assert.Len(value, 1)
|
||||
require.Len(t, value, 1)
|
||||
|
||||
var actual [][]string
|
||||
expected := [][]string{
|
||||
{"bar", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"foo", "*", "foo", "*"},
|
||||
{"foo", "*", "*", "*"},
|
||||
{"default", "*", "default", "bar"},
|
||||
{"default", "*", "default", "*"},
|
||||
}
|
||||
for _, ixn := range value["foo/bar"] {
|
||||
for _, ixn := range value["bar"] {
|
||||
actual = append(actual, []string{
|
||||
ixn.SourceNS,
|
||||
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) {
|
||||
|
@ -187,16 +180,14 @@ func TestIntentionsMatch_noName(t *testing.T) {
|
|||
func TestIntentionsCheck_basic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
|
||||
// Create some intentions
|
||||
{
|
||||
insert := [][]string{
|
||||
{"foo", "*", "foo", "*"},
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"bar", "*", "foo", "bar"},
|
||||
{"default", "*", "default", "baz"},
|
||||
{"default", "*", "default", "bar"},
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
|
@ -213,30 +204,30 @@ func TestIntentionsCheck_basic(t *testing.T) {
|
|||
|
||||
// Create
|
||||
var reply string
|
||||
require.Nil(a.RPC("Intention.Apply", &ixn, &reply))
|
||||
require.NoError(t, a.RPC("Intention.Apply", &ixn, &reply))
|
||||
}
|
||||
}
|
||||
|
||||
// Request matching intention
|
||||
{
|
||||
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()
|
||||
obj, err := a.srv.IntentionCheck(resp, req)
|
||||
require.Nil(err)
|
||||
require.NoError(t, err)
|
||||
value := obj.(*structs.IntentionQueryCheckResponse)
|
||||
require.False(value.Allowed)
|
||||
require.False(t, value.Allowed)
|
||||
}
|
||||
|
||||
// Request non-matching intention
|
||||
{
|
||||
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()
|
||||
obj, err := a.srv.IntentionCheck(resp, req)
|
||||
require.Nil(err)
|
||||
require.NoError(t, err)
|
||||
value := obj.(*structs.IntentionQueryCheckResponse)
|
||||
require.True(value.Allowed)
|
||||
require.True(t, value.Allowed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,12 +473,10 @@ func TestParseIntentionMatchEntry(t *testing.T) {
|
|||
{
|
||||
"foo",
|
||||
structs.IntentionMatchEntry{
|
||||
Namespace: structs.IntentionDefaultNamespace,
|
||||
Name: "foo",
|
||||
Name: "foo",
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"foo/bar",
|
||||
structs.IntentionMatchEntry{
|
||||
|
@ -496,7 +485,6 @@ func TestParseIntentionMatchEntry(t *testing.T) {
|
|||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"foo/bar/baz",
|
||||
structs.IntentionMatchEntry{},
|
||||
|
@ -507,7 +495,8 @@ func TestParseIntentionMatchEntry(t *testing.T) {
|
|||
for _, tc := range cases {
|
||||
t.Run(tc.Input, func(t *testing.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)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
@ -2,6 +2,7 @@ package structs
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -85,6 +86,18 @@ type Intention struct {
|
|||
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) {
|
||||
type Alias Intention
|
||||
aux := &struct {
|
||||
|
@ -246,6 +259,10 @@ func (ixn *Intention) CanRead(authz acl.Authorizer) bool {
|
|||
}
|
||||
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 != "" {
|
||||
ixn.FillAuthzContext(&authzContext, false)
|
||||
if authz.IntentionRead(ixn.SourceName, &authzContext) == acl.Allow {
|
||||
|
@ -431,6 +448,10 @@ type IntentionQueryRequest struct {
|
|||
// return allowed/deny based on an exact match.
|
||||
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
|
||||
QueryOptions
|
||||
}
|
||||
|
@ -507,6 +528,31 @@ type IntentionQueryCheckResponse struct {
|
|||
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
|
||||
// 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
|
||||
|
|
|
@ -38,3 +38,10 @@ func (ixn *Intention) DefaultNamespaces(_ *EnterpriseMeta) {
|
|||
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
|
||||
}
|
||||
|
||||
func (m *EnterpriseMeta) NamespaceOrEmpty() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func EnterpriseMetaInitializer(_ string) EnterpriseMeta {
|
||||
return emptyEnterpriseMeta
|
||||
}
|
||||
|
|
|
@ -270,6 +270,39 @@ func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *
|
|||
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
|
||||
// structure must be empty and a generate ID will be returned on
|
||||
// success.
|
||||
|
|
|
@ -41,7 +41,7 @@ func TestAPI_ConnectIntentionCreateListGetUpdateDelete(t *testing.T) {
|
|||
require.Equal(ixn, actual)
|
||||
|
||||
// Update it
|
||||
ixn.SourceNS = ixn.SourceNS + "-different"
|
||||
ixn.SourceName = ixn.SourceName + "-different"
|
||||
_, err = connect.IntentionUpdate(ixn, nil)
|
||||
require.NoError(err)
|
||||
|
||||
|
@ -91,12 +91,9 @@ func TestAPI_ConnectIntentionMatch(t *testing.T) {
|
|||
// Create
|
||||
{
|
||||
insert := [][]string{
|
||||
{"foo", "*"},
|
||||
{"foo", "bar"},
|
||||
{"foo", "baz"}, // shouldn't match
|
||||
{"bar", "bar"}, // shouldn't match
|
||||
{"bar", "*"}, // shouldn't match
|
||||
{"*", "*"},
|
||||
{"default", "*"},
|
||||
{"default", "bar"},
|
||||
{"default", "baz"}, // shouldn't match
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
|
@ -112,14 +109,17 @@ func TestAPI_ConnectIntentionMatch(t *testing.T) {
|
|||
// Match it
|
||||
result, _, err := connect.IntentionMatch(&IntentionMatch{
|
||||
By: IntentionMatchDestination,
|
||||
Names: []string{"foo/bar"},
|
||||
Names: []string{"bar"},
|
||||
}, nil)
|
||||
require.Nil(err)
|
||||
require.Len(result, 1)
|
||||
|
||||
var actual [][]string
|
||||
expected := [][]string{{"foo", "bar"}, {"foo", "*"}, {"*", "*"}}
|
||||
for _, ixn := range result["foo/bar"] {
|
||||
expected := [][]string{
|
||||
{"default", "bar"},
|
||||
{"default", "*"},
|
||||
}
|
||||
for _, ixn := range result["bar"] {
|
||||
actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName})
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,8 @@ func TestAPI_ConnectIntentionCheck(t *testing.T) {
|
|||
// Create
|
||||
{
|
||||
insert := [][]string{
|
||||
{"foo", "*", "foo", "bar"},
|
||||
{"default", "*", "default", "bar", "deny"},
|
||||
{"default", "foo", "default", "bar", "allow"},
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
|
@ -147,39 +148,39 @@ func TestAPI_ConnectIntentionCheck(t *testing.T) {
|
|||
ixn.SourceName = v[1]
|
||||
ixn.DestinationNS = v[2]
|
||||
ixn.DestinationName = v[3]
|
||||
ixn.Action = IntentionActionDeny
|
||||
ixn.Action = IntentionAction(v[4])
|
||||
id, _, err := connect.IntentionCreate(ixn, nil)
|
||||
require.Nil(err)
|
||||
require.NotEmpty(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Match it
|
||||
// Match the deny rule
|
||||
{
|
||||
result, _, err := connect.IntentionCheck(&IntentionCheck{
|
||||
Source: "foo/qux",
|
||||
Destination: "foo/bar",
|
||||
Source: "default/qux",
|
||||
Destination: "default/bar",
|
||||
}, nil)
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
require.False(result)
|
||||
}
|
||||
|
||||
// Match it (non-matching)
|
||||
// Match the allow rule
|
||||
{
|
||||
result, _, err := connect.IntentionCheck(&IntentionCheck{
|
||||
Source: "bar/qux",
|
||||
Destination: "foo/bar",
|
||||
Source: "default/foo",
|
||||
Destination: "default/bar",
|
||||
}, nil)
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
require.True(result)
|
||||
}
|
||||
}
|
||||
|
||||
func testIntention() *Intention {
|
||||
return &Intention{
|
||||
SourceNS: "eng",
|
||||
SourceNS: "default",
|
||||
SourceName: "api",
|
||||
DestinationNS: "eng",
|
||||
DestinationNS: "default",
|
||||
DestinationName: "db",
|
||||
Precedence: 9,
|
||||
Action: IntentionActionAllow,
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/hashicorp/consul/command/intention/create"
|
||||
"github.com/hashicorp/consul/command/intention/finder"
|
||||
"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))
|
||||
|
||||
// Check for an existing intention.
|
||||
ixnFinder := finder.Finder{Client: client}
|
||||
existing, err := ixnFinder.Find(c.ingressGateway, c.service)
|
||||
existing, _, err := client.Connect().IntentionGetExact(c.ingressGateway, c.service, nil)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error looking up existing intention: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -31,6 +31,7 @@ func (c *cmd) init() {
|
|||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
flags.Merge(c.flags, c.http.NamespaceFlags())
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/hashicorp/consul/command/intention/finder"
|
||||
"github.com/hashicorp/consul/command/intention"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
|
@ -54,6 +54,7 @@ func (c *cmd) init() {
|
|||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
flags.Merge(c.flags, c.http.NamespaceFlags())
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
|
@ -88,20 +89,21 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Create the finder in case we need it
|
||||
find := &finder.Finder{Client: client}
|
||||
|
||||
// Go through and create each intention
|
||||
for _, ixn := range ixns {
|
||||
// If replace is set to true, then perform an update operation.
|
||||
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 {
|
||||
c.UI.Error(fmt.Sprintf(
|
||||
"Error looking up intention for replacement with source %q "+
|
||||
"and destination %q: %s",
|
||||
ixn.SourceString(),
|
||||
ixn.DestinationString(),
|
||||
intention.FormatSource(ixn),
|
||||
intention.FormatDestination(ixn),
|
||||
err))
|
||||
return 1
|
||||
}
|
||||
|
@ -113,8 +115,8 @@ func (c *cmd) Run(args []string) int {
|
|||
c.UI.Error(fmt.Sprintf(
|
||||
"Error replacing intention with source %q "+
|
||||
"and destination %q: %s",
|
||||
ixn.SourceString(),
|
||||
ixn.DestinationString(),
|
||||
intention.FormatSource(ixn),
|
||||
intention.FormatDestination(ixn),
|
||||
err))
|
||||
return 1
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ func (c *cmd) init() {
|
|||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
flags.Merge(c.flags, c.http.NamespaceFlags())
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
|
@ -47,8 +48,7 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the intention ID to load
|
||||
f := &finder.Finder{Client: client}
|
||||
id, err := f.IDFromArgs(c.flags.Args())
|
||||
id, err := finder.IDFromArgs(client, c.flags.Args())
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -2,38 +2,21 @@ package finder
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
// Finder finds intentions by a src/dst exact match. There is currently
|
||||
// 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
|
||||
// IDFromArgs returns the intention ID for the given CLI args. An error is returned
|
||||
// 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) {
|
||||
case 1:
|
||||
return args[0], nil
|
||||
|
||||
case 2:
|
||||
ixn, err := f.Find(args[0], args[1])
|
||||
ixn, _, err := client.Connect().IntentionGetExact(
|
||||
args[0], args[1], nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -49,44 +32,3 @@ func (f *Finder) IDFromArgs(args []string) (string, error) {
|
|||
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"
|
||||
)
|
||||
|
||||
func TestFinder(t *testing.T) {
|
||||
func TestIDFromArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
a := agent.NewTestAgent(t, ``)
|
||||
defer a.Shutdown()
|
||||
client := a.Client()
|
||||
|
@ -20,30 +19,26 @@ func TestFinder(t *testing.T) {
|
|||
var ids []string
|
||||
{
|
||||
insert := [][]string{
|
||||
{"a", "b", "c", "d"},
|
||||
{"a", "b"},
|
||||
}
|
||||
|
||||
for _, v := range insert {
|
||||
ixn := &api.Intention{
|
||||
SourceNS: v[0],
|
||||
SourceName: v[1],
|
||||
DestinationNS: v[2],
|
||||
DestinationName: v[3],
|
||||
SourceName: v[0],
|
||||
DestinationName: v[1],
|
||||
Action: api.IntentionActionAllow,
|
||||
}
|
||||
|
||||
id, _, err := client.Connect().IntentionCreate(ixn, nil)
|
||||
require.NoError(err)
|
||||
require.NoError(t, err)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
finder := &Finder{Client: client}
|
||||
ixn, err := finder.Find("a/b", "c/d")
|
||||
require.NoError(err)
|
||||
require.Equal(ids[0], ixn.ID)
|
||||
id, err := IDFromArgs(client, []string{"a", "b"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ids[0], id)
|
||||
|
||||
ixn, err = finder.Find("a/c", "c/d")
|
||||
require.NoError(err)
|
||||
require.Nil(ixn)
|
||||
_, err = IDFromArgs(client, []string{"c", "d"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
flags.Merge(c.flags, c.http.NamespaceFlags())
|
||||
c.help = flags.Usage(help, c.flags)
|
||||
}
|
||||
|
||||
|
@ -50,8 +51,7 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the intention ID to load
|
||||
f := &finder.Finder{Client: client}
|
||||
id, err := f.IDFromArgs(c.flags.Args())
|
||||
id, err := finder.IDFromArgs(client, c.flags.Args())
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -40,6 +40,7 @@ func (c *cmd) init() {
|
|||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
flags.Merge(c.flags, c.http.NamespaceFlags())
|
||||
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
|
||||
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.
|
||||
The intention destination is always a Consul service, unlike the source.
|
||||
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.
|
||||
This can be only "consul" today to represent a Consul service.
|
||||
|
||||
- `Action` `(string: <required>)` - This is one of "allow" or "deny" for
|
||||
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
|
||||
tooling.
|
||||
|
||||
- `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
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
```shell-session
|
||||
|
@ -366,9 +462,19 @@ The table below shows this endpoint's support for
|
|||
|
||||
- `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
|
||||
|
||||
|
@ -422,6 +528,15 @@ The table below shows this endpoint's support for
|
|||
|
||||
- `name` `(string: <required>)` - Specifies a name to match. This parameter
|
||||
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
|
||||
|
||||
|
|
|
@ -22,10 +22,16 @@ intention read permissions and don't evaluate the result.
|
|||
|
||||
Usage: `consul intention check [options] SRC DST`
|
||||
|
||||
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
|
||||
|
||||
#### API Options
|
||||
|
||||
@include 'http_api_options_client.mdx'
|
||||
|
||||
#### Enterprise Options
|
||||
|
||||
@include 'http_api_namespace_options.mdx'
|
||||
|
||||
## Examples
|
||||
|
||||
```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] -f FILE...`
|
||||
|
||||
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
|
||||
|
||||
#### API Options
|
||||
|
||||
@include 'http_api_options_client.mdx'
|
||||
|
||||
#### Enterprise Options
|
||||
|
||||
@include 'http_api_namespace_options.mdx'
|
||||
|
||||
#### Intention Create Options
|
||||
|
||||
- `-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] ID`
|
||||
|
||||
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
|
||||
|
||||
#### API Options
|
||||
|
||||
@include 'http_api_options_client.mdx'
|
||||
|
||||
#### Enterprise Options
|
||||
|
||||
@include 'http_api_namespace_options.mdx'
|
||||
|
||||
## Examples
|
||||
|
||||
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] ID`
|
||||
|
||||
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
|
||||
|
||||
#### API Options
|
||||
|
||||
@include 'http_api_options_client.mdx'
|
||||
|
||||
#### Enterprise Options
|
||||
|
||||
@include 'http_api_namespace_options.mdx'
|
||||
|
||||
## Examples
|
||||
|
||||
```shell-session
|
||||
|
|
|
@ -64,3 +64,16 @@ Find all intentions for communicating to the "db" service:
|
|||
```shell-session
|
||||
$ 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`
|
||||
|
||||
`SRC` and `DST` can both take [several forms](/docs/commands/intention#source-and-destination-naming).
|
||||
|
||||
#### API Options
|
||||
|
||||
@include 'http_api_options_client.mdx'
|
||||
|
||||
#### Enterprise Options
|
||||
|
||||
@include 'http_api_namespace_options.mdx'
|
||||
|
||||
#### Intention Match Options
|
||||
|
||||
- `-destination` - Match by destination.
|
||||
|
|
Loading…
Reference in New Issue