b63a0f3909
Improves tests from #12362 These tests try to setup the following concurrent scenario: 1. (goroutine 1) execute read RPC with index=0 2. (goroutine 1) get response from (1) @ index=10 3. (goroutine 1) execute read RPC with index=10 and block 4. (goroutine 2) WHILE (3) is blocking, start slamming the system with stray writes that will cause the WatchSet to wakeup 5. (goroutine 2) after doing all writes, shut down the reader above 6. (goroutine 1) stops reading, double checks that it only ever woke up once (from 1)
2183 lines
59 KiB
Go
2183 lines
59 KiB
Go
package consul
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
)
|
|
|
|
// Test basic creation
|
|
func TestIntentionApply_new(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: structs.IntentionDefaultNamespace,
|
|
SourceName: "test",
|
|
DestinationNS: structs.IntentionDefaultNamespace,
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
SourceType: structs.IntentionSourceConsul,
|
|
Meta: map[string]string{},
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Record now to check created at time
|
|
now := time.Now()
|
|
|
|
// Create
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
require.NotEmpty(t, reply)
|
|
|
|
// Read
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
actual := resp.Intentions[0]
|
|
require.Equal(t, resp.Index, actual.ModifyIndex)
|
|
require.WithinDuration(t, now, actual.CreatedAt, 5*time.Second)
|
|
require.WithinDuration(t, 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
|
|
//nolint:staticcheck
|
|
ixn.Intention.UpdatePrecedence()
|
|
// Partition fields will be normalized on Intention.Get
|
|
ixn.Intention.FillPartitionAndNamespace(nil, true)
|
|
require.Equal(t, ixn.Intention, actual)
|
|
}
|
|
|
|
// Rename should fail
|
|
t.Run("renaming the destination should fail", func(t *testing.T) {
|
|
// Setup a basic record to create
|
|
ixn2 := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpUpdate,
|
|
Intention: &structs.Intention{
|
|
ID: ixn.Intention.ID,
|
|
SourceNS: structs.IntentionDefaultNamespace,
|
|
SourceName: "test",
|
|
DestinationNS: structs.IntentionDefaultNamespace,
|
|
DestinationName: "test-updated",
|
|
Action: structs.IntentionActionAllow,
|
|
SourceType: structs.IntentionSourceConsul,
|
|
Meta: map[string]string{},
|
|
},
|
|
}
|
|
|
|
var reply string
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn2, &reply)
|
|
testutil.RequireErrorContains(t, err, "Cannot modify Destination partition/namespace/name for an intention once it exists.")
|
|
})
|
|
}
|
|
|
|
// Test the source type defaults
|
|
func TestIntentionApply_defaultSourceType(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: structs.IntentionDefaultNamespace,
|
|
SourceName: "test",
|
|
DestinationNS: structs.IntentionDefaultNamespace,
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Create
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
require.NotEmpty(t, reply)
|
|
|
|
// Read
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
actual := resp.Intentions[0]
|
|
require.Equal(t, structs.IntentionSourceConsul, actual.SourceType)
|
|
}
|
|
}
|
|
|
|
// Shouldn't be able to create with an ID set
|
|
func TestIntentionApply_createWithID(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
ID: generateUUID(),
|
|
SourceName: "test",
|
|
DestinationName: "test2",
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Create
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
require.NotNil(t, err)
|
|
require.Contains(t, err, "ID must be empty")
|
|
}
|
|
|
|
// Test basic updating
|
|
func TestIntentionApply_updateGood(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: structs.IntentionDefaultNamespace,
|
|
SourceName: "test",
|
|
DestinationNS: structs.IntentionDefaultNamespace,
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
SourceType: structs.IntentionSourceConsul,
|
|
Meta: map[string]string{},
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Create
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
require.NotEmpty(t, reply)
|
|
|
|
// Read CreatedAt
|
|
var createdAt time.Time
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
actual := resp.Intentions[0]
|
|
createdAt = actual.CreatedAt
|
|
}
|
|
|
|
// Sleep a bit so that the updated at will definitely be different, not much
|
|
time.Sleep(1 * time.Millisecond)
|
|
|
|
// Update
|
|
ixn.Op = structs.IntentionOpUpdate
|
|
ixn.Intention.ID = reply
|
|
ixn.Intention.Description = "updated"
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
|
|
// Read
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
actual := resp.Intentions[0]
|
|
require.Equal(t, createdAt, actual.CreatedAt)
|
|
require.WithinDuration(t, 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
|
|
//nolint:staticcheck
|
|
ixn.Intention.UpdatePrecedence()
|
|
// Partition fields will be normalized on Intention.Get
|
|
ixn.Intention.FillPartitionAndNamespace(nil, true)
|
|
require.Equal(t, ixn.Intention, actual)
|
|
}
|
|
}
|
|
|
|
// Shouldn't be able to update a non-existent intention
|
|
func TestIntentionApply_updateNonExist(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpUpdate,
|
|
Intention: &structs.Intention{
|
|
ID: generateUUID(),
|
|
SourceName: "test",
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Create
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
require.NotNil(t, err)
|
|
require.Contains(t, err, "Cannot modify non-existent intention")
|
|
}
|
|
|
|
// Test basic deleting
|
|
func TestIntentionApply_deleteGood(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
}
|
|
var reply string
|
|
|
|
// Delete a non existent intention should return an error
|
|
testutil.RequireErrorContains(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &structs.IntentionRequest{
|
|
Op: structs.IntentionOpDelete,
|
|
Intention: &structs.Intention{
|
|
ID: generateUUID(),
|
|
},
|
|
}, &reply), "Cannot delete non-existent intention")
|
|
|
|
// Create
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
require.NotEmpty(t, reply)
|
|
|
|
// Delete
|
|
ixn.Op = structs.IntentionOpDelete
|
|
ixn.Intention.ID = reply
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
|
|
// Read
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
|
require.NotNil(t, err)
|
|
require.Contains(t, err, ErrIntentionNotFound.Error())
|
|
}
|
|
}
|
|
|
|
func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
defaultEntMeta := structs.DefaultEnterpriseMetaInDefaultPartition()
|
|
|
|
// Force "test" to be L7-capable.
|
|
{
|
|
args := structs.ConfigEntryRequest{
|
|
Datacenter: "dc1",
|
|
Entry: &structs.ServiceConfigEntry{
|
|
Kind: structs.ServiceDefaults,
|
|
Name: "test",
|
|
Protocol: "http",
|
|
},
|
|
}
|
|
|
|
var out bool
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
|
|
require.True(t, out)
|
|
}
|
|
|
|
opApply := func(req *structs.IntentionRequest) error {
|
|
req.Datacenter = "dc1"
|
|
var ignored string
|
|
return msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &ignored)
|
|
}
|
|
|
|
opGet := func(req *structs.IntentionQueryRequest) (*structs.IndexedIntentions, error) {
|
|
req.Datacenter = "dc1"
|
|
var resp structs.IndexedIntentions
|
|
if err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
opList := func() (*structs.IndexedIntentions, error) {
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
EnterpriseMeta: *structs.WildcardEnterpriseMetaInDefaultPartition(),
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
if err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
configEntryUpsert := func(entry *structs.ServiceIntentionsConfigEntry) error {
|
|
req := &structs.ConfigEntryRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.ConfigEntryUpsert,
|
|
Entry: entry,
|
|
}
|
|
var ignored bool
|
|
return msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", req, &ignored)
|
|
}
|
|
|
|
getConfigEntry := func(kind, name string) (*structs.ServiceIntentionsConfigEntry, error) {
|
|
state := s1.fsm.State()
|
|
_, entry, err := state.ConfigEntry(nil, kind, name, defaultEntMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ixn, ok := entry.(*structs.ServiceIntentionsConfigEntry)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected type: %T", entry)
|
|
}
|
|
return ixn, nil
|
|
}
|
|
|
|
// Setup a basic record to create
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
Description: "original",
|
|
},
|
|
}))
|
|
|
|
// Read it back.
|
|
{
|
|
resp, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, resp.Intentions, 1)
|
|
got := resp.Intentions[0]
|
|
require.Equal(t, "original", got.Description)
|
|
|
|
// L4
|
|
require.Equal(t, structs.IntentionActionAllow, got.Action)
|
|
require.Empty(t, got.Permissions)
|
|
|
|
// Verify it is in the new-style.
|
|
require.Empty(t, got.ID)
|
|
require.True(t, got.CreatedAt.IsZero())
|
|
require.True(t, got.UpdatedAt.IsZero())
|
|
}
|
|
|
|
// Double check that there's only 1.
|
|
{
|
|
resp, err := opList()
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Intentions, 1)
|
|
}
|
|
|
|
// Verify the config entry structure is expected.
|
|
{
|
|
entry, err := getConfigEntry(structs.ServiceIntentions, "test")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entry)
|
|
|
|
expect := &structs.ServiceIntentionsConfigEntry{
|
|
Kind: structs.ServiceIntentions,
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Sources: []*structs.SourceIntention{
|
|
{
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Action: structs.IntentionActionAllow,
|
|
Description: "original",
|
|
Precedence: 9,
|
|
Type: structs.IntentionSourceConsul,
|
|
},
|
|
},
|
|
RaftIndex: entry.RaftIndex,
|
|
}
|
|
|
|
require.Equal(t, expect, entry)
|
|
}
|
|
|
|
// Update in place.
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionAllow,
|
|
Description: "updated",
|
|
},
|
|
}))
|
|
|
|
// Read it back.
|
|
{
|
|
resp, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, resp.Intentions, 1)
|
|
got := resp.Intentions[0]
|
|
require.Equal(t, "updated", got.Description)
|
|
|
|
// L4
|
|
require.Equal(t, structs.IntentionActionAllow, got.Action)
|
|
require.Empty(t, got.Permissions)
|
|
|
|
// Verify it is in the new-style.
|
|
require.Empty(t, got.ID)
|
|
require.True(t, got.CreatedAt.IsZero())
|
|
require.True(t, got.UpdatedAt.IsZero())
|
|
}
|
|
|
|
// Double check that there's only 1.
|
|
{
|
|
resp, err := opList()
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Intentions, 1)
|
|
}
|
|
|
|
// Create a second one sharing the same destination
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
Description: "original-2",
|
|
Permissions: []*structs.IntentionPermission{
|
|
{
|
|
Action: structs.IntentionActionAllow,
|
|
HTTP: &structs.IntentionHTTPPermission{
|
|
PathExact: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
// Read it back.
|
|
{
|
|
resp, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, resp.Intentions, 1)
|
|
got := resp.Intentions[0]
|
|
require.Equal(t, "original-2", got.Description)
|
|
|
|
// L7
|
|
require.Empty(t, got.Action)
|
|
require.Equal(t, []*structs.IntentionPermission{
|
|
{
|
|
Action: structs.IntentionActionAllow,
|
|
HTTP: &structs.IntentionHTTPPermission{
|
|
PathExact: "/foo",
|
|
},
|
|
},
|
|
}, got.Permissions)
|
|
|
|
// Verify it is in the new-style.
|
|
require.Empty(t, got.ID)
|
|
require.True(t, got.CreatedAt.IsZero())
|
|
require.True(t, got.UpdatedAt.IsZero())
|
|
}
|
|
|
|
// Double check that there's 2 now.
|
|
{
|
|
resp, err := opList()
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Intentions, 2)
|
|
}
|
|
|
|
// Verify the config entry structure is expected.
|
|
{
|
|
entry, err := getConfigEntry(structs.ServiceIntentions, "test")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entry)
|
|
|
|
expect := &structs.ServiceIntentionsConfigEntry{
|
|
Kind: structs.ServiceIntentions,
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Sources: []*structs.SourceIntention{
|
|
{
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Action: structs.IntentionActionAllow,
|
|
Description: "updated",
|
|
Precedence: 9,
|
|
Type: structs.IntentionSourceConsul,
|
|
},
|
|
{
|
|
Name: "assay",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Description: "original-2",
|
|
Precedence: 9,
|
|
Type: structs.IntentionSourceConsul,
|
|
Permissions: []*structs.IntentionPermission{
|
|
{
|
|
Action: structs.IntentionActionAllow,
|
|
HTTP: &structs.IntentionHTTPPermission{
|
|
PathExact: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
RaftIndex: entry.RaftIndex,
|
|
}
|
|
|
|
require.Equal(t, expect, entry)
|
|
}
|
|
|
|
// Delete a non existent intention should act like it did work
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpDelete,
|
|
Intention: &structs.Intention{
|
|
SourceName: "ghost",
|
|
DestinationName: "phantom",
|
|
},
|
|
}))
|
|
|
|
// Delete the original
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpDelete,
|
|
Intention: &structs.Intention{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
},
|
|
}))
|
|
|
|
// Read it back (not found)
|
|
{
|
|
_, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "test",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
testutil.RequireErrorContains(t, err, ErrIntentionNotFound.Error())
|
|
}
|
|
|
|
// Double check that there's 1 again.
|
|
{
|
|
resp, err := opList()
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Intentions, 1)
|
|
}
|
|
|
|
// Verify the config entry structure is expected.
|
|
{
|
|
entry, err := getConfigEntry(structs.ServiceIntentions, "test")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entry)
|
|
|
|
expect := &structs.ServiceIntentionsConfigEntry{
|
|
Kind: structs.ServiceIntentions,
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Sources: []*structs.SourceIntention{
|
|
{
|
|
Name: "assay",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Description: "original-2",
|
|
Precedence: 9,
|
|
Type: structs.IntentionSourceConsul,
|
|
Permissions: []*structs.IntentionPermission{
|
|
{
|
|
Action: structs.IntentionActionAllow,
|
|
HTTP: &structs.IntentionHTTPPermission{
|
|
PathExact: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
RaftIndex: entry.RaftIndex,
|
|
}
|
|
|
|
require.Equal(t, expect, entry)
|
|
}
|
|
|
|
// Set metadata on the config entry directly.
|
|
{
|
|
require.NoError(t, configEntryUpsert(&structs.ServiceIntentionsConfigEntry{
|
|
Kind: structs.ServiceIntentions,
|
|
Name: "test",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"zim": "gir",
|
|
},
|
|
Sources: []*structs.SourceIntention{
|
|
{
|
|
Name: "assay",
|
|
EnterpriseMeta: *defaultEntMeta,
|
|
Action: structs.IntentionActionDeny,
|
|
Description: "original-2",
|
|
Precedence: 9,
|
|
Type: structs.IntentionSourceConsul,
|
|
},
|
|
},
|
|
}))
|
|
}
|
|
|
|
// Attempt to create a new intention and set the metadata.
|
|
{
|
|
err := opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "foo",
|
|
DestinationName: "bar",
|
|
Action: structs.IntentionActionDeny,
|
|
Meta: map[string]string{"horseshoe": "crab"},
|
|
},
|
|
})
|
|
testutil.RequireErrorContains(t, err, "Meta must not be specified")
|
|
}
|
|
|
|
// Attempt to update an intention and change the metadata.
|
|
{
|
|
err := opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionDeny,
|
|
Description: "original-3",
|
|
Meta: map[string]string{"horseshoe": "crab"},
|
|
},
|
|
})
|
|
testutil.RequireErrorContains(t, err, "Meta must not be specified, or should be unchanged during an update.")
|
|
}
|
|
|
|
// Try again with the same metadata.
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionDeny,
|
|
Description: "original-3",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"zim": "gir",
|
|
},
|
|
},
|
|
}))
|
|
|
|
// Read it back.
|
|
{
|
|
resp, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, resp.Intentions, 1)
|
|
got := resp.Intentions[0]
|
|
require.Equal(t, "original-3", got.Description)
|
|
require.Equal(t, map[string]string{
|
|
"foo": "bar",
|
|
"zim": "gir",
|
|
}, got.Meta)
|
|
|
|
// Verify it is in the new-style.
|
|
require.Empty(t, got.ID)
|
|
require.True(t, got.CreatedAt.IsZero())
|
|
require.True(t, got.UpdatedAt.IsZero())
|
|
}
|
|
|
|
// Try again with NO metadata.
|
|
require.NoError(t, opApply(&structs.IntentionRequest{
|
|
Op: structs.IntentionOpUpsert,
|
|
Intention: &structs.Intention{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
Action: structs.IntentionActionDeny,
|
|
Description: "original-4",
|
|
},
|
|
}))
|
|
|
|
// Read it back.
|
|
{
|
|
resp, err := opGet(&structs.IntentionQueryRequest{
|
|
Exact: &structs.IntentionQueryExact{
|
|
SourceName: "assay",
|
|
DestinationName: "test",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, resp.Intentions, 1)
|
|
got := resp.Intentions[0]
|
|
require.Equal(t, "original-4", got.Description)
|
|
require.Equal(t, map[string]string{
|
|
"foo": "bar",
|
|
"zim": "gir",
|
|
}, got.Meta)
|
|
|
|
// Verify it is in the new-style.
|
|
require.Empty(t, got.ID)
|
|
require.True(t, got.CreatedAt.IsZero())
|
|
require.True(t, got.UpdatedAt.IsZero())
|
|
}
|
|
}
|
|
|
|
// Test apply with a deny ACL
|
|
func TestIntentionApply_aclDeny(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
rules := `
|
|
service "foobar" {
|
|
policy = "deny"
|
|
intentions = "write"
|
|
}`
|
|
token := createToken(t, codec, rules)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "foobar"
|
|
|
|
// Create without a token should error since default deny
|
|
var reply string
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
|
|
// Now add the token and try again.
|
|
ixn.WriteRequest.Token = token
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
|
|
// Read
|
|
ixn.Intention.ID = reply
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
QueryOptions: structs.QueryOptions{Token: "root"},
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
actual := resp.Intentions[0]
|
|
require.Equal(t, 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
|
|
//nolint:staticcheck
|
|
ixn.Intention.UpdatePrecedence()
|
|
require.Equal(t, ixn.Intention, actual)
|
|
}
|
|
}
|
|
|
|
func TestIntention_WildcardACLEnforcement(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
_, srv, codec := testACLServerWithConfig(t, nil, false)
|
|
waitForLeaderEstablishment(t, srv)
|
|
|
|
// create some test policies.
|
|
|
|
writeToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "write" }`)
|
|
require.NoError(t, err)
|
|
readToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "read" }`)
|
|
require.NoError(t, err)
|
|
exactToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "*" { policy = "deny" intentions = "write" }`)
|
|
require.NoError(t, err)
|
|
wildcardPrefixToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "*" { policy = "deny" intentions = "write" }`)
|
|
require.NoError(t, err)
|
|
fooToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "foo" { policy = "deny" intentions = "write" }`)
|
|
require.NoError(t, err)
|
|
denyToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "deny" }`)
|
|
require.NoError(t, err)
|
|
|
|
doIntentionCreate := func(t *testing.T, token string, dest string, deny bool) string {
|
|
t.Helper()
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: dest,
|
|
Action: structs.IntentionActionAllow,
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
WriteRequest: structs.WriteRequest{Token: token},
|
|
}
|
|
var reply string
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
if deny {
|
|
require.Error(t, err)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
return ""
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, reply)
|
|
return reply
|
|
}
|
|
}
|
|
|
|
t.Run("deny-write-for-read-token", func(t *testing.T) {
|
|
// This tests ensures that tokens with only read access to all intentions
|
|
// cannot create a wildcard intention
|
|
doIntentionCreate(t, readToken.SecretID, "*", true)
|
|
})
|
|
|
|
t.Run("deny-write-for-exact-wildcard-rule", func(t *testing.T) {
|
|
// This test ensures that having a rules like:
|
|
// service "*" {
|
|
// intentions = "write"
|
|
// }
|
|
// will not actually allow creating an intention with a wildcard service name
|
|
doIntentionCreate(t, exactToken.SecretID, "*", true)
|
|
})
|
|
|
|
t.Run("deny-write-for-prefix-wildcard-rule", func(t *testing.T) {
|
|
// This test ensures that having a rules like:
|
|
// service_prefix "*" {
|
|
// intentions = "write"
|
|
// }
|
|
// will not actually allow creating an intention with a wildcard service name
|
|
doIntentionCreate(t, wildcardPrefixToken.SecretID, "*", true)
|
|
})
|
|
|
|
var intentionID string
|
|
allowWriteOk := t.Run("allow-write", func(t *testing.T) {
|
|
// tests that a token with all the required privileges can create
|
|
// intentions with a wildcard destination
|
|
intentionID = doIntentionCreate(t, writeToken.SecretID, "*", false)
|
|
})
|
|
|
|
requireAllowWrite := func(t *testing.T) {
|
|
t.Helper()
|
|
if !allowWriteOk {
|
|
t.Skip("Skipping because the allow-write subtest failed")
|
|
}
|
|
}
|
|
|
|
doIntentionRead := func(t *testing.T, token string, deny bool) {
|
|
t.Helper()
|
|
requireAllowWrite(t)
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: intentionID,
|
|
QueryOptions: structs.QueryOptions{Token: token},
|
|
}
|
|
|
|
var resp structs.IndexedIntentions
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
|
if deny {
|
|
require.Error(t, err)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Intentions, 1)
|
|
require.Equal(t, "*", resp.Intentions[0].DestinationName)
|
|
}
|
|
}
|
|
|
|
t.Run("allow-read-for-write-token", func(t *testing.T) {
|
|
doIntentionRead(t, writeToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-read-for-read-token", func(t *testing.T) {
|
|
doIntentionRead(t, readToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-read-for-exact-wildcard-token", func(t *testing.T) {
|
|
// this is allowed because, the effect of the policy is to grant
|
|
// intention:write on the service named "*". When reading the
|
|
// intention we will validate that the token has read permissions
|
|
// for any intention that would match the wildcard.
|
|
doIntentionRead(t, exactToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-read-for-prefix-wildcard-token", func(t *testing.T) {
|
|
// this is allowed for the same reasons as for the
|
|
// exact-wildcard-token case
|
|
doIntentionRead(t, wildcardPrefixToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("deny-read-for-deny-token", func(t *testing.T) {
|
|
doIntentionRead(t, denyToken.SecretID, true)
|
|
})
|
|
|
|
doIntentionList := func(t *testing.T, token string, deny bool) {
|
|
t.Helper()
|
|
requireAllowWrite(t)
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
QueryOptions: structs.QueryOptions{Token: token},
|
|
}
|
|
|
|
var resp structs.IndexedIntentions
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)
|
|
// even with permission denied this should return success but with an empty list
|
|
require.NoError(t, err)
|
|
if deny {
|
|
require.Empty(t, resp.Intentions)
|
|
} else {
|
|
require.Len(t, resp.Intentions, 1)
|
|
require.Equal(t, "*", resp.Intentions[0].DestinationName)
|
|
}
|
|
}
|
|
|
|
t.Run("allow-list-for-write-token", func(t *testing.T) {
|
|
doIntentionList(t, writeToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-list-for-read-token", func(t *testing.T) {
|
|
doIntentionList(t, readToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-list-for-exact-wildcard-token", func(t *testing.T) {
|
|
doIntentionList(t, exactToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-list-for-prefix-wildcard-token", func(t *testing.T) {
|
|
doIntentionList(t, wildcardPrefixToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("deny-list-for-deny-token", func(t *testing.T) {
|
|
doIntentionList(t, denyToken.SecretID, true)
|
|
})
|
|
|
|
doIntentionMatch := func(t *testing.T, token string, deny bool) {
|
|
t.Helper()
|
|
requireAllowWrite(t)
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Match: &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{
|
|
Namespace: "default",
|
|
Name: "*",
|
|
},
|
|
},
|
|
},
|
|
QueryOptions: structs.QueryOptions{Token: token},
|
|
}
|
|
|
|
var resp structs.IndexedIntentionMatches
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)
|
|
if deny {
|
|
require.Error(t, err)
|
|
require.Empty(t, resp.Matches)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Matches, 1)
|
|
require.Len(t, resp.Matches[0], 1)
|
|
require.Equal(t, "*", resp.Matches[0][0].DestinationName)
|
|
}
|
|
}
|
|
|
|
t.Run("allow-match-for-write-token", func(t *testing.T) {
|
|
doIntentionMatch(t, writeToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-match-for-read-token", func(t *testing.T) {
|
|
doIntentionMatch(t, readToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-match-for-exact-wildcard-token", func(t *testing.T) {
|
|
doIntentionMatch(t, exactToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("allow-match-for-prefix-wildcard-token", func(t *testing.T) {
|
|
doIntentionMatch(t, wildcardPrefixToken.SecretID, false)
|
|
})
|
|
|
|
t.Run("deny-match-for-deny-token", func(t *testing.T) {
|
|
doIntentionMatch(t, denyToken.SecretID, true)
|
|
})
|
|
|
|
// Since we can't rename the destination, create a new intention for the rest of this test.
|
|
wildIntentionID := intentionID
|
|
fooIntentionID := doIntentionCreate(t, writeToken.SecretID, "foo", false)
|
|
|
|
doIntentionUpdate := func(t *testing.T, token string, intentionID, dest, description string, deny bool) {
|
|
t.Helper()
|
|
requireAllowWrite(t)
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpUpdate,
|
|
Intention: &structs.Intention{
|
|
ID: intentionID,
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: dest,
|
|
Description: description,
|
|
Action: structs.IntentionActionAllow,
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
WriteRequest: structs.WriteRequest{Token: token},
|
|
}
|
|
var reply string
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
if deny {
|
|
require.Error(t, err)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
t.Run("deny-update-for-foo-token", func(t *testing.T) {
|
|
doIntentionUpdate(t, fooToken.SecretID, wildIntentionID, "*", "wild-desc", true)
|
|
})
|
|
|
|
t.Run("allow-update-for-prefix-token", func(t *testing.T) {
|
|
// This tests that the prefix token can edit wildcard intentions and regular intentions.
|
|
doIntentionUpdate(t, writeToken.SecretID, fooIntentionID, "foo", "foo-desc-two", false)
|
|
doIntentionUpdate(t, writeToken.SecretID, wildIntentionID, "*", "wild-desc-two", false)
|
|
})
|
|
|
|
doIntentionDelete := func(t *testing.T, token string, intentionID string, deny bool) {
|
|
t.Helper()
|
|
requireAllowWrite(t)
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpDelete,
|
|
Intention: &structs.Intention{
|
|
ID: intentionID,
|
|
},
|
|
WriteRequest: structs.WriteRequest{Token: token},
|
|
}
|
|
var reply string
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
if deny {
|
|
require.Error(t, err)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
t.Run("deny-delete-for-read-token", func(t *testing.T) {
|
|
doIntentionDelete(t, readToken.SecretID, fooIntentionID, true)
|
|
})
|
|
|
|
t.Run("deny-delete-for-exact-wildcard-rule", func(t *testing.T) {
|
|
// This test ensures that having a rules like:
|
|
// service "*" {
|
|
// intentions = "write"
|
|
// }
|
|
// will not actually allow deleting an intention with a wildcard service name
|
|
doIntentionDelete(t, exactToken.SecretID, fooIntentionID, true)
|
|
})
|
|
|
|
t.Run("deny-delete-for-prefix-wildcard-rule", func(t *testing.T) {
|
|
// This test ensures that having a rules like:
|
|
// service_prefix "*" {
|
|
// intentions = "write"
|
|
// }
|
|
// will not actually allow creating an intention with a wildcard service name
|
|
doIntentionDelete(t, wildcardPrefixToken.SecretID, fooIntentionID, true)
|
|
})
|
|
|
|
t.Run("allow-delete", func(t *testing.T) {
|
|
// tests that a token with all the required privileges can delete
|
|
// intentions with a wildcard destination
|
|
doIntentionDelete(t, writeToken.SecretID, fooIntentionID, false)
|
|
})
|
|
}
|
|
|
|
// Test apply with delete and a default deny ACL
|
|
func TestIntentionApply_aclDelete(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
rules := `
|
|
service "foobar" {
|
|
policy = "deny"
|
|
intentions = "write"
|
|
}`
|
|
token := createToken(t, codec, rules)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "foobar"
|
|
ixn.WriteRequest.Token = token
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, 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)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
|
|
// Try again with the original token. This should go through.
|
|
ixn.WriteRequest.Token = token
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
|
|
// Verify it is gone
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
|
|
require.NotNil(t, err)
|
|
require.Contains(t, err.Error(), ErrIntentionNotFound.Error())
|
|
}
|
|
}
|
|
|
|
// Test apply with update and a default deny ACL
|
|
func TestIntentionApply_aclUpdate(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
rules := `
|
|
service "foobar" {
|
|
policy = "deny"
|
|
intentions = "write"
|
|
}`
|
|
token := createToken(t, codec, rules)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "foobar"
|
|
ixn.WriteRequest.Token = token
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, 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)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
|
|
// Try again with the original token; this should go through.
|
|
ixn.WriteRequest.Token = token
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
}
|
|
|
|
// Test apply with a management token
|
|
func TestIntentionApply_aclManagement(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "foobar"
|
|
ixn.WriteRequest.Token = "root"
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
ixn.Intention.ID = reply
|
|
|
|
// Update
|
|
ixn.Op = structs.IntentionOpUpdate
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
|
|
// Delete
|
|
ixn.Op = structs.IntentionOpDelete
|
|
require.Nil(t, 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) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
rules := `
|
|
service "foobar" {
|
|
policy = "deny"
|
|
intentions = "write"
|
|
}`
|
|
token := createToken(t, codec, rules)
|
|
|
|
// Setup a basic record to create
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "bar"
|
|
ixn.WriteRequest.Token = "root"
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, 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.Intention.DestinationName = "foo"
|
|
ixn.WriteRequest.Token = token
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
}
|
|
|
|
// Test reading with ACLs
|
|
func TestIntentionGet_acl(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Create an ACL with service write permissions. This will grant
|
|
// 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{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = "foobar"
|
|
ixn.WriteRequest.Token = "root"
|
|
|
|
// Create
|
|
var reply string
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
ixn.Intention.ID = reply
|
|
|
|
t.Run("Read by ID without token should be error", func(t *testing.T) {
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
}
|
|
|
|
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 ID with token should work", func(t *testing.T) {
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
IntentionID: ixn.Intention.ID,
|
|
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)
|
|
})
|
|
|
|
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) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Test with no intentions inserted yet
|
|
{
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
|
require.NotNil(t, resp.Intentions)
|
|
require.Len(t, resp.Intentions, 0)
|
|
}
|
|
}
|
|
|
|
// Test listing with ACLs
|
|
func TestIntentionList_acl(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, testServerACLConfig)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "foo" { policy = "write" }`)
|
|
require.NoError(t, err)
|
|
|
|
// Create a few records
|
|
for _, name := range []string{"foobar", "bar", "baz"} {
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.SourceNS = "default"
|
|
ixn.Intention.DestinationNS = "default"
|
|
ixn.Intention.DestinationName = name
|
|
ixn.WriteRequest.Token = TestDefaultInitialManagementToken
|
|
|
|
// Create
|
|
var reply string
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
}
|
|
|
|
// Test with no token
|
|
t.Run("no-token", func(t *testing.T) {
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
|
require.Len(t, resp.Intentions, 0)
|
|
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
|
|
})
|
|
|
|
// Test with management token
|
|
t.Run("initial-management-token", func(t *testing.T) {
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
QueryOptions: structs.QueryOptions{Token: TestDefaultInitialManagementToken},
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
|
require.Len(t, resp.Intentions, 3)
|
|
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
|
|
})
|
|
|
|
// Test with user token
|
|
t.Run("user-token", func(t *testing.T) {
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
|
}
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
require.True(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
|
|
})
|
|
|
|
t.Run("filtered", func(t *testing.T) {
|
|
req := &structs.IntentionListRequest{
|
|
Datacenter: "dc1",
|
|
QueryOptions: structs.QueryOptions{
|
|
Token: TestDefaultInitialManagementToken,
|
|
Filter: "DestinationName == foobar",
|
|
},
|
|
}
|
|
|
|
var resp structs.IndexedIntentions
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
|
require.Len(t, resp.Intentions, 1)
|
|
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
|
|
})
|
|
}
|
|
|
|
// Test basic matching. We don't need to exhaustively test inputs since this
|
|
// is tested in the agent/consul/state package.
|
|
func TestIntentionMatch_good(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Create some records
|
|
{
|
|
insert := [][]string{
|
|
{"default", "*", "default", "*"},
|
|
{"default", "*", "default", "bar"},
|
|
{"default", "*", "default", "baz"}, // shouldn't match
|
|
}
|
|
|
|
for _, v := range insert {
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: v[0],
|
|
SourceName: v[1],
|
|
DestinationNS: v[2],
|
|
DestinationName: v[3],
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
}
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
}
|
|
}
|
|
|
|
// Match
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Match: &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{Name: "bar"},
|
|
},
|
|
},
|
|
}
|
|
var resp structs.IndexedIntentionMatches
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
|
|
require.Len(t, resp.Matches, 1)
|
|
|
|
expected := [][]string{
|
|
{"default", "*", "default", "bar"},
|
|
{"default", "*", "default", "*"},
|
|
}
|
|
var actual [][]string
|
|
for _, ixn := range resp.Matches[0] {
|
|
actual = append(actual, []string{
|
|
ixn.SourceNS,
|
|
ixn.SourceName,
|
|
ixn.DestinationNS,
|
|
ixn.DestinationName,
|
|
})
|
|
}
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestIntentionMatch_BlockOnNoChange(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
_, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.DevMode = true // keep it in ram to make it 10x faster on macos
|
|
})
|
|
|
|
codec := rpcClient(t, s1)
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
run := func(t *testing.T, dataPrefix string, expectMatches int) {
|
|
rpcBlockingQueryTestHarness(t,
|
|
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
|
|
args := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Match: &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{Name: "bar"},
|
|
},
|
|
},
|
|
}
|
|
args.QueryOptions.MinQueryIndex = minQueryIndex
|
|
|
|
var out structs.IndexedIntentionMatches
|
|
errCh := channelCallRPC(s1, "Intention.Match", args, &out, func() error {
|
|
if len(out.Matches) != 1 {
|
|
return fmt.Errorf("expected 1 match got %d", len(out.Matches))
|
|
}
|
|
if len(out.Matches[0]) != expectMatches {
|
|
return fmt.Errorf("expected %d inner matches got %d", expectMatches, len(out.Matches[0]))
|
|
}
|
|
return nil
|
|
})
|
|
return &out.QueryMeta, errCh
|
|
},
|
|
func(i int) <-chan error {
|
|
var out string
|
|
return channelCallRPC(s1, "Intention.Apply", &structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
// {"default", "*", "default", "baz"}, // shouldn't match
|
|
SourceNS: "default",
|
|
SourceName: "*",
|
|
DestinationNS: "default",
|
|
DestinationName: fmt.Sprintf(dataPrefix+"%d", i),
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
}, &out, nil)
|
|
},
|
|
)
|
|
}
|
|
|
|
runStep(t, "test the errNotFound path", func(t *testing.T) {
|
|
run(t, "other", 0)
|
|
})
|
|
|
|
// Create some records
|
|
{
|
|
insert := [][]string{
|
|
{"default", "*", "default", "*"},
|
|
{"default", "*", "default", "bar"},
|
|
{"default", "*", "default", "baz"}, // shouldn't match
|
|
}
|
|
|
|
for _, v := range insert {
|
|
var out string
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: v[0],
|
|
SourceName: v[1],
|
|
DestinationNS: v[2],
|
|
DestinationName: v[3],
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
}, &out))
|
|
}
|
|
}
|
|
|
|
runStep(t, "test the errNotChanged path", func(t *testing.T) {
|
|
run(t, "completely-different-other", 2)
|
|
})
|
|
}
|
|
|
|
// Test matching with ACLs
|
|
func TestIntentionMatch_acl(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
_, srv, codec := testACLServerWithConfig(t, nil, false)
|
|
waitForLeaderEstablishment(t, srv)
|
|
|
|
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "bar" { policy = "write" }`)
|
|
require.NoError(t, err)
|
|
|
|
// Create some records
|
|
{
|
|
insert := []string{
|
|
"*",
|
|
"bar",
|
|
"baz",
|
|
}
|
|
|
|
for _, v := range insert {
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: structs.TestIntention(t),
|
|
}
|
|
ixn.Intention.DestinationName = v
|
|
ixn.WriteRequest.Token = TestDefaultInitialManagementToken
|
|
|
|
// Create
|
|
var reply string
|
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
}
|
|
}
|
|
|
|
// Test with no token
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Match: &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{
|
|
Namespace: "default",
|
|
Name: "bar",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
var resp structs.IndexedIntentionMatches
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
require.Len(t, resp.Matches, 0)
|
|
}
|
|
|
|
// Test with proper token
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Match: &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{
|
|
Namespace: "default",
|
|
Name: "bar",
|
|
},
|
|
},
|
|
},
|
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
|
}
|
|
var resp structs.IndexedIntentionMatches
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
|
|
require.Len(t, resp.Matches, 1)
|
|
|
|
expected := []string{"bar", "*"}
|
|
var actual []string
|
|
for _, ixn := range resp.Matches[0] {
|
|
actual = append(actual, ixn.DestinationName)
|
|
}
|
|
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
}
|
|
|
|
// Test the Check method defaults to allow with no ACL set.
|
|
func TestIntentionCheck_defaultNoACL(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServer(t)
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Test
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceName: "bar",
|
|
DestinationName: "qux",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
}
|
|
var resp structs.IntentionQueryCheckResponse
|
|
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) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Check
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceName: "bar",
|
|
DestinationName: "qux",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
}
|
|
req.Token = "root"
|
|
var resp structs.IntentionQueryCheckResponse
|
|
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) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "allow"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
// Check
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceName: "bar",
|
|
DestinationName: "qux",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
}
|
|
req.Token = "root"
|
|
var resp structs.IntentionQueryCheckResponse
|
|
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) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
c.PrimaryDatacenter = "dc1"
|
|
c.ACLsEnabled = true
|
|
c.ACLInitialManagementToken = "root"
|
|
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
|
|
})
|
|
defer os.RemoveAll(dir1)
|
|
defer s1.Shutdown()
|
|
codec := rpcClient(t, s1)
|
|
defer codec.Close()
|
|
|
|
waitForLeaderEstablishment(t, s1)
|
|
|
|
rules := `
|
|
service "bar" {
|
|
policy = "read"
|
|
}`
|
|
token := createToken(t, codec, rules)
|
|
|
|
// Check
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceName: "qux",
|
|
DestinationName: "baz",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
}
|
|
req.Token = token
|
|
var resp structs.IntentionQueryCheckResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)
|
|
require.True(t, acl.IsErrPermissionDenied(err))
|
|
}
|
|
|
|
// Test the Check method returns allow/deny properly.
|
|
func TestIntentionCheck_match(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
_, srv, codec := testACLServerWithConfig(t, nil, false)
|
|
waitForLeaderEstablishment(t, srv)
|
|
|
|
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "api" { policy = "read" }`)
|
|
require.NoError(t, err)
|
|
|
|
// Create some intentions
|
|
{
|
|
insert := [][]string{
|
|
{"web", "db"},
|
|
{"api", "db"},
|
|
{"web", "api"},
|
|
}
|
|
|
|
for _, v := range insert {
|
|
ixn := structs.IntentionRequest{
|
|
Datacenter: "dc1",
|
|
Op: structs.IntentionOpCreate,
|
|
Intention: &structs.Intention{
|
|
SourceNS: "default",
|
|
SourceName: v[0],
|
|
DestinationNS: "default",
|
|
DestinationName: v[1],
|
|
Action: structs.IntentionActionAllow,
|
|
},
|
|
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
|
|
}
|
|
// Create
|
|
var reply string
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
|
}
|
|
}
|
|
|
|
// Check
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceNS: "default",
|
|
SourceName: "web",
|
|
DestinationNS: "default",
|
|
DestinationName: "api",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
|
}
|
|
var resp structs.IntentionQueryCheckResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
|
require.True(t, resp.Allowed)
|
|
|
|
// Test no match for sanity
|
|
{
|
|
req := &structs.IntentionQueryRequest{
|
|
Datacenter: "dc1",
|
|
Check: &structs.IntentionQueryCheck{
|
|
SourceNS: "default",
|
|
SourceName: "db",
|
|
DestinationNS: "default",
|
|
DestinationName: "api",
|
|
SourceType: structs.IntentionSourceConsul,
|
|
},
|
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
|
}
|
|
var resp structs.IntentionQueryCheckResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
|
|
require.False(t, resp.Allowed)
|
|
}
|
|
}
|
|
|
|
func TestEqualStringMaps(t *testing.T) {
|
|
m1 := map[string]string{
|
|
"foo": "a",
|
|
}
|
|
m2 := map[string]string{
|
|
"foo": "a",
|
|
"bar": "b",
|
|
}
|
|
var m3 map[string]string
|
|
|
|
m4 := map[string]string{
|
|
"dog": "",
|
|
}
|
|
|
|
m5 := map[string]string{
|
|
"cat": "",
|
|
}
|
|
|
|
tests := []struct {
|
|
a map[string]string
|
|
b map[string]string
|
|
result bool
|
|
}{
|
|
{m1, m1, true},
|
|
{m2, m2, true},
|
|
{m1, m2, false},
|
|
{m2, m1, false},
|
|
{m2, m2, true},
|
|
{m3, m1, false},
|
|
{m3, m3, true},
|
|
{m4, m5, false},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
actual := equalStringMaps(test.a, test.b)
|
|
if actual != test.result {
|
|
t.Fatalf("case %d, expected %v, got %v", i, test.result, actual)
|
|
}
|
|
}
|
|
}
|