open-consul/agent/structs/config_entry_intentions_test.go
R.B. Boyer 35c4efd220
connect: support defining intentions using layer 7 criteria (#8839)
Extend Consul’s intentions model to allow for request-based access control enforcement for HTTP-like protocols in addition to the existing connection-based enforcement for unspecified protocols (e.g. tcp).
2020-10-06 17:09:13 -05:00

1443 lines
35 KiB
Go

package structs
import (
"fmt"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func generateUUID() (ret string) {
var err error
if ret, err = uuid.GenerateUUID(); err != nil {
panic(fmt.Sprintf("Unable to generate a UUID, %v", err))
}
return ret
}
func TestServiceIntentionsConfigEntry(t *testing.T) {
type testcase struct {
entry *ServiceIntentionsConfigEntry
legacy bool
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceIntentionsConfigEntry)
}
legacyIDs := []string{
generateUUID(),
generateUUID(),
generateUUID(),
}
defaultMeta := DefaultEnterpriseMeta()
fooName := NewServiceName("foo", defaultMeta)
cases := map[string]testcase{
"nil": {
entry: nil,
normalizeErr: "config entry is nil",
},
"no name": {
entry: &ServiceIntentionsConfigEntry{},
validateErr: "Name is required",
},
"dest name has partial wildcard": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test*",
},
validateErr: "Name: wildcard character '*' cannot be used with partial values",
},
"empty": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
},
validateErr: "At least one source is required",
},
"source specified twice": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
},
{
Name: "foo",
Action: IntentionActionDeny,
},
},
},
validateErr: `Sources[1] defines "` + fooName.String() + `" more than once`,
},
"no source name": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Action: IntentionActionAllow,
},
},
},
validateErr: `Sources[0].Name is required`,
},
"source name has partial wildcard": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo*",
Action: IntentionActionAllow,
},
},
},
validateErr: `Sources[0].Name: wildcard character '*' cannot be used with partial values`,
},
"description too long": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
Description: strings.Repeat("x", 513),
},
},
},
validateErr: `Sources[0].Description exceeds maximum length 512`,
},
"config entry meta not allowed on legacy writes": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
},
},
Meta: map[string]string{
"key1": "val1",
},
},
validateErr: `Meta must be omitted for legacy intention writes`,
},
"config entry meta too many keys": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
},
},
Meta: makeStringMap(65, 5, 5),
},
validateErr: `Meta exceeds maximum element count 64`,
},
"config entry meta key too large": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
},
},
Meta: makeStringMap(64, 129, 5),
},
validateErr: `exceeds maximum length 128`,
},
"config entry meta value too large": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
},
},
Meta: makeStringMap(64, 128, 513),
},
validateErr: `exceeds maximum length 512`,
},
"config entry meta value just big enough": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
},
},
Meta: makeStringMap(64, 128, 512),
},
},
"legacy meta not allowed": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
Description: strings.Repeat("x", 512),
LegacyMeta: map[string]string{ // stray Meta will be dropped
"old": "data",
},
},
},
},
validateErr: "Sources[0].LegacyMeta must be omitted",
},
"legacy meta too many keys": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
LegacyMeta: makeStringMap(65, 5, 5),
},
},
},
validateErr: `Sources[0].Meta exceeds maximum element count 64`,
},
"legacy meta key too large": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
LegacyMeta: makeStringMap(64, 129, 5),
},
},
},
validateErr: `exceeds maximum length 128`,
},
"legacy meta value too large": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
LegacyMeta: makeStringMap(64, 128, 513),
},
},
},
validateErr: `exceeds maximum length 512`,
},
"legacy meta value just big enough": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Name: "foo",
Action: IntentionActionAllow,
LegacyMeta: makeStringMap(64, 128, 512),
},
},
},
},
"legacy ID is required in legacy mode": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
Description: strings.Repeat("x", 512),
},
},
},
validateErr: "Sources[0].LegacyID must be set",
},
"action required for L4": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
},
},
},
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`,
},
"action must be allow or deny for L4": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: "blah",
Description: strings.Repeat("x", 512),
},
},
},
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`,
},
"action must not be set for L7": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Action: IntentionActionAllow,
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{PathExact: "/"},
},
},
},
},
},
validateErr: `Sources[0].Action must be omitted if Permissions are specified`,
},
"permission action must be set": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
HTTP: &IntentionHTTPPermission{PathExact: "/"},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`,
},
"permission action must allow or deny": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: "blah",
HTTP: &IntentionHTTPPermission{PathExact: "/"},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`,
},
"permission missing http": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP is required`,
},
"permission has too many path components (1)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathExact: "/",
PathPrefix: "/a",
// PathRegex: "/b",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
},
"permission has too many path components (2)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathExact: "/",
// PathPrefix: "/a",
PathRegex: "/b",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
},
"permission has too many path components (3)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
// PathExact: "/",
PathPrefix: "/a",
PathRegex: "/b",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
},
"permission has too many path components (4)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathExact: "/",
PathPrefix: "/a",
PathRegex: "/b",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
},
"permission has invalid path exact": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathExact: "x",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.PathExact doesn't start with '/': "x"`,
},
"permission has invalid path prefix": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathPrefix: "x",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.PathPrefix doesn't start with '/': "x"`,
},
"permission header missing name": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Description: strings.Repeat("x", 512),
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{Exact: "foo"},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] missing required Name field`,
},
"permission header has too many parts (1)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (2)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (3)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (4)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (5)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (6)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (7)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (8)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (9)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (10)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission header has too many parts (11)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
Regex: "foo",
Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
},
"permission invalid method": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Methods: []string{"YOINK"},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains an invalid method "YOINK"`,
},
"permission repeated method": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Methods: []string{"POST", "PUT", "POST"},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains "POST" more than once`,
},
"permission should not be empty (1)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{},
Methods: []string{},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`,
},
"permission should not be empty (2)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`,
},
"permission kitchen sink": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathPrefix: "/foo",
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Exact: "foo",
},
{
Name: "x-xyz",
Present: true,
Invert: true,
},
},
Methods: []string{"POST", "PUT", "GET"},
},
},
},
},
},
},
},
"permissions not allowed on wildcarded destinations": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
// TODO: ent
Name: WildcardSpecifier,
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathPrefix: "/foo",
},
},
},
},
},
},
validateErr: `Sources[0].Permissions cannot be specified on intentions with wildcarded destinations`,
},
"L4 normalize": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0], // stray ID will be dropped
Name: WildcardSpecifier,
Action: IntentionActionDeny,
},
{
Name: "foo",
Action: IntentionActionAllow,
},
{
Name: "bar",
Action: IntentionActionDeny,
},
},
Meta: map[string]string{
"key1": "val1",
"key2": "val2",
},
},
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) {
// Note the stable precedence sort has been applied here.
assert.Equal(t, []*SourceIntention{
{
Name: "foo",
EnterpriseMeta: *defaultMeta,
Action: IntentionActionAllow,
Precedence: 9,
Type: IntentionSourceConsul,
},
{
Name: "bar",
EnterpriseMeta: *defaultMeta,
Action: IntentionActionDeny,
Precedence: 9,
Type: IntentionSourceConsul,
},
{
Name: WildcardSpecifier,
EnterpriseMeta: *defaultMeta,
Action: IntentionActionDeny,
Precedence: 8,
Type: IntentionSourceConsul,
},
}, entry.Sources)
assert.Equal(t, map[string]string{
"key1": "val1",
"key2": "val2",
}, entry.Meta)
},
},
"L4 legacy normalize": {
legacy: true,
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: WildcardSpecifier,
Action: IntentionActionDeny,
LegacyID: legacyIDs[0],
},
{
Name: "foo",
Action: IntentionActionAllow,
LegacyID: legacyIDs[1],
LegacyMeta: map[string]string{
"key1": "val1",
"key2": "val2",
},
},
{
Name: "bar",
Action: IntentionActionDeny,
LegacyID: legacyIDs[2],
},
},
},
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) {
require.Len(t, entry.Sources, 3)
assert.False(t, entry.Sources[0].LegacyCreateTime.IsZero())
assert.False(t, entry.Sources[0].LegacyUpdateTime.IsZero())
assert.False(t, entry.Sources[1].LegacyCreateTime.IsZero())
assert.False(t, entry.Sources[1].LegacyUpdateTime.IsZero())
assert.False(t, entry.Sources[2].LegacyCreateTime.IsZero())
assert.False(t, entry.Sources[2].LegacyUpdateTime.IsZero())
assert.Equal(t, []*SourceIntention{
{
LegacyID: legacyIDs[1],
Name: "foo",
EnterpriseMeta: *defaultMeta,
Action: IntentionActionAllow,
Precedence: 9,
Type: IntentionSourceConsul,
LegacyMeta: map[string]string{
"key1": "val1",
"key2": "val2",
},
LegacyCreateTime: entry.Sources[0].LegacyCreateTime,
LegacyUpdateTime: entry.Sources[0].LegacyUpdateTime,
},
{
LegacyID: legacyIDs[2],
Name: "bar",
EnterpriseMeta: *defaultMeta,
Action: IntentionActionDeny,
Precedence: 9,
Type: IntentionSourceConsul,
LegacyMeta: map[string]string{},
LegacyCreateTime: entry.Sources[1].LegacyCreateTime,
LegacyUpdateTime: entry.Sources[1].LegacyUpdateTime,
},
{
LegacyID: legacyIDs[0],
Name: WildcardSpecifier,
EnterpriseMeta: *defaultMeta,
Action: IntentionActionDeny,
Precedence: 8,
Type: IntentionSourceConsul,
LegacyMeta: map[string]string{},
LegacyCreateTime: entry.Sources[2].LegacyCreateTime,
LegacyUpdateTime: entry.Sources[2].LegacyUpdateTime,
},
}, entry.Sources)
},
},
"L4 validate": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0], // stray ID will be dropped
Name: WildcardSpecifier,
Action: IntentionActionDeny,
},
{
Name: "foo",
Action: IntentionActionAllow,
},
{
Name: "bar",
Action: IntentionActionDeny,
},
},
Meta: map[string]string{
"key1": "val1",
"key2": "val2",
},
},
},
"L7 normalize": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "bar",
Permissions: []*IntentionPermission{
{
Action: IntentionActionDeny,
HTTP: &IntentionHTTPPermission{
Methods: []string{
"get", "post",
},
},
},
},
},
},
},
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) {
assert.Equal(t, []*SourceIntention{
{
Name: "bar",
EnterpriseMeta: *defaultMeta,
Precedence: 9,
Type: IntentionSourceConsul,
Permissions: []*IntentionPermission{
{
Action: IntentionActionDeny,
HTTP: &IntentionHTTPPermission{
Methods: []string{
"GET", "POST",
},
},
},
},
},
}, entry.Sources)
},
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
var err error
if tc.legacy {
err = tc.entry.LegacyNormalize()
} else {
err = tc.entry.Normalize()
}
if tc.normalizeErr != "" {
// require.Error(t, err)
// require.Contains(t, err.Error(), tc.normalizeErr)
testutil.RequireErrorContains(t, err, tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
if tc.legacy {
err = tc.entry.LegacyValidate()
} else {
err = tc.entry.Validate()
}
if tc.validateErr != "" {
// require.Error(t, err)
// require.Contains(t, err.Error(), tc.validateErr)
testutil.RequireErrorContains(t, err, tc.validateErr)
return
}
require.NoError(t, err)
})
}
}
func makeStringMap(keys, keySize, valSize int) map[string]string {
m := make(map[string]string)
for i := 0; i < keys; i++ {
base := fmt.Sprintf("%d:", i)
if len(base) > keySize || len(base) > valSize {
panic("makeStringMap called with incompatible inputs")
}
// this is not performant
if keySize > valSize {
base = strings.Repeat(base, keySize)
} else {
base = strings.Repeat(base, valSize)
}
m[base[0:keySize]] = base[0:valSize]
}
return m
}
func TestMigrateIntentions(t *testing.T) {
type testcase struct {
in Intentions
expect []*ServiceIntentionsConfigEntry
}
legacyIDs := []string{
generateUUID(),
generateUUID(),
generateUUID(),
}
anyTime := time.Now().UTC()
cases := map[string]testcase{
"nil": {},
"one": {
in: Intentions{
{
ID: legacyIDs[0],
Description: "desc",
SourceName: "foo",
DestinationName: "bar",
SourceType: IntentionSourceConsul,
Action: IntentionActionAllow,
Meta: map[string]string{
"key1": "val1",
},
Precedence: 9,
CreatedAt: anyTime,
UpdatedAt: anyTime,
},
},
expect: []*ServiceIntentionsConfigEntry{
{
Kind: ServiceIntentions,
Name: "bar",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Description: "desc",
Name: "foo",
Type: IntentionSourceConsul,
Action: IntentionActionAllow,
LegacyMeta: map[string]string{
"key1": "val1",
},
},
},
},
},
},
"two in same": {
in: Intentions{
{
ID: legacyIDs[0],
Description: "desc",
SourceName: "foo",
DestinationName: "bar",
SourceType: IntentionSourceConsul,
Action: IntentionActionAllow,
Meta: map[string]string{
"key1": "val1",
},
Precedence: 9,
CreatedAt: anyTime,
UpdatedAt: anyTime,
},
{
ID: legacyIDs[1],
Description: "desc2",
SourceName: "*",
DestinationName: "bar",
SourceType: IntentionSourceConsul,
Action: IntentionActionDeny,
Meta: map[string]string{
"key2": "val2",
},
Precedence: 9,
CreatedAt: anyTime,
UpdatedAt: anyTime,
},
},
expect: []*ServiceIntentionsConfigEntry{
{
Kind: ServiceIntentions,
Name: "bar",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Description: "desc",
Name: "foo",
Type: IntentionSourceConsul,
Action: IntentionActionAllow,
LegacyMeta: map[string]string{
"key1": "val1",
},
},
{
LegacyID: legacyIDs[1],
Description: "desc2",
Name: "*",
Type: IntentionSourceConsul,
Action: IntentionActionDeny,
LegacyMeta: map[string]string{
"key2": "val2",
},
},
},
},
},
},
"two in different": {
in: Intentions{
{
ID: legacyIDs[0],
Description: "desc",
SourceName: "foo",
DestinationName: "bar",
SourceType: IntentionSourceConsul,
Action: IntentionActionAllow,
Meta: map[string]string{
"key1": "val1",
},
Precedence: 9,
CreatedAt: anyTime,
UpdatedAt: anyTime,
},
{
ID: legacyIDs[1],
Description: "desc2",
SourceName: "*",
DestinationName: "bar2",
SourceType: IntentionSourceConsul,
Action: IntentionActionDeny,
Meta: map[string]string{
"key2": "val2",
},
Precedence: 9,
CreatedAt: anyTime,
UpdatedAt: anyTime,
},
},
expect: []*ServiceIntentionsConfigEntry{
{
Kind: ServiceIntentions,
Name: "bar",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[0],
Description: "desc",
Name: "foo",
Type: IntentionSourceConsul,
Action: IntentionActionAllow,
LegacyMeta: map[string]string{
"key1": "val1",
},
},
},
},
{
Kind: ServiceIntentions,
Name: "bar2",
Sources: []*SourceIntention{
{
LegacyID: legacyIDs[1],
Description: "desc2",
Name: "*",
Type: IntentionSourceConsul,
Action: IntentionActionDeny,
LegacyMeta: map[string]string{
"key2": "val2",
},
},
},
},
},
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
got := MigrateIntentions(tc.in)
require.ElementsMatch(t, tc.expect, got)
})
}
}