consul/agent/structs/intention_test.go

536 lines
15 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"sort"
2018-03-03 17:43:37 +00:00
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
)
func TestIntention_ACLs(t *testing.T) {
type testCase struct {
intention Intention
rules string
read bool
write bool
}
cases := map[string]testCase{
"all-denied": {
intention: Intention{
SourceNS: "default",
SourceName: "web",
DestinationNS: "default",
DestinationName: "api",
},
read: false,
write: false,
},
"deny-write-read-dest": {
rules: `service "api" { policy = "deny" intentions = "read" }`,
intention: Intention{
SourceNS: "default",
SourceName: "web",
DestinationNS: "default",
DestinationName: "api",
},
read: true,
write: false,
},
"deny-write-read-source": {
rules: `service "web" { policy = "deny" intentions = "read" }`,
intention: Intention{
SourceNS: "default",
SourceName: "web",
DestinationNS: "default",
DestinationName: "api",
},
read: true,
write: false,
},
"allow-write-with-dest-write": {
rules: `service "api" { policy = "deny" intentions = "write" }`,
intention: Intention{
SourceNS: "default",
SourceName: "web",
DestinationNS: "default",
DestinationName: "api",
},
read: true,
write: true,
},
"deny-write-with-source-write": {
rules: `service "web" { policy = "deny" intentions = "write" }`,
intention: Intention{
SourceNS: "default",
SourceName: "web",
DestinationNS: "default",
DestinationName: "api",
},
read: true,
write: false,
},
"deny-wildcard-write-allow-read": {
rules: `service "*" { policy = "deny" intentions = "write" }`,
intention: Intention{
SourceNS: "default",
SourceName: "*",
DestinationNS: "default",
DestinationName: "*",
},
// technically having been granted read/write on any intention will allow
// read access for this rule
read: true,
write: false,
},
"allow-wildcard-write": {
rules: `service_prefix "" { policy = "deny" intentions = "write" }`,
intention: Intention{
SourceNS: "default",
SourceName: "*",
DestinationNS: "default",
DestinationName: "*",
},
read: true,
write: true,
},
"allow-wildcard-read": {
rules: `service "foo" { policy = "deny" intentions = "read" }`,
intention: Intention{
SourceNS: "default",
SourceName: "*",
DestinationNS: "default",
DestinationName: "*",
},
read: true,
write: false,
},
}
config := acl.Config{
WildcardName: WildcardSpecifier,
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
authz, err := acl.NewAuthorizerFromRules(tcase.rules, &config, nil)
require.NoError(t, err)
require.Equal(t, tcase.read, tcase.intention.CanRead(authz))
require.Equal(t, tcase.write, tcase.intention.CanWrite(authz))
})
}
}
2018-03-03 17:43:37 +00:00
func TestIntentionValidate(t *testing.T) {
cases := []struct {
Name string
Modify func(*Intention)
Err string
}{
{
"long description",
func(x *Intention) {
x.Description = strings.Repeat("x", metaValueMaxLength+1)
},
"description exceeds",
},
{
"no action set",
func(x *Intention) { x.Action = "" },
"action must be set",
},
{
"no SourceNS",
func(x *Intention) { x.SourceNS = "" },
"SourceNS must be set",
},
{
"no SourceName",
func(x *Intention) { x.SourceName = "" },
"SourceName must be set",
},
{
"no DestinationNS",
func(x *Intention) { x.DestinationNS = "" },
"DestinationNS must be set",
},
{
"no DestinationName",
func(x *Intention) { x.DestinationName = "" },
"DestinationName must be set",
},
{
"SourceNS partial wildcard",
func(x *Intention) { x.SourceNS = "foo*" },
"partial value",
},
{
"SourceName partial wildcard",
func(x *Intention) { x.SourceName = "foo*" },
"partial value",
},
{
"SourceName exact following wildcard",
func(x *Intention) {
x.SourceNS = "*"
x.SourceName = "foo"
},
"follow wildcard",
},
{
"DestinationNS partial wildcard",
func(x *Intention) { x.DestinationNS = "foo*" },
"partial value",
},
{
"DestinationName partial wildcard",
func(x *Intention) { x.DestinationName = "foo*" },
"partial value",
},
{
"DestinationName exact following wildcard",
func(x *Intention) {
x.DestinationNS = "*"
x.DestinationName = "foo"
},
"follow wildcard",
},
{
"SourceType is not set",
func(x *Intention) { x.SourceType = "" },
"SourceType must",
},
{
"SourceType is other",
func(x *Intention) { x.SourceType = IntentionSourceType("other") },
"SourceType must",
},
2018-03-03 17:43:37 +00:00
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
ixn := TestIntention(t)
tc.Modify(ixn)
err := ixn.Validate()
assert.Equal(t, err != nil, tc.Err != "", err)
2018-03-03 17:43:37 +00:00
if err == nil {
return
}
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
2018-03-03 17:43:37 +00:00
})
}
}
func TestIntentionPrecedenceSorter(t *testing.T) {
type fields struct {
SrcSamenessGroup string
SrcPeer string
SrcNS string
SrcN string
DstNS string
DstN string
}
cases := []struct {
Name string
Input []fields
Expected []fields
}{
{
"exhaustive list",
[]fields{
// Sameness fields
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
// Peer fields
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
},
[]fields{
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
},
},
{
"tiebreak deterministically",
[]fields{
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "a"},
{SrcNS: "b", SrcN: "a", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "b", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "b", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "a"},
},
[]fields{
// Exact matches first in lexicographical order (arbitrary but
// deterministic)
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "a", DstNS: "b", DstN: "a"},
{SrcNS: "a", SrcN: "b", DstNS: "a", DstN: "a"},
{SrcNS: "b", SrcN: "a", DstNS: "a", DstN: "a"},
// Wildcards next, lexicographical
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "b"},
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
var input Intentions
for _, v := range tc.Input {
input = append(input, &Intention{
SourceSamenessGroup: v.SrcSamenessGroup,
SourcePeer: v.SrcPeer,
SourceNS: v.SrcNS,
SourceName: v.SrcN,
DestinationNS: v.DstNS,
DestinationName: v.DstN,
})
}
// Set all the precedence values
for _, ixn := range input {
ixn.UpdatePrecedence()
}
// Sort
sort.Sort(IntentionPrecedenceSorter(input))
// Get back into a comparable form
var actual []fields
for _, v := range input {
actual = append(actual, fields{
SrcSamenessGroup: v.SourceSamenessGroup,
SrcPeer: v.SourcePeer,
SrcNS: v.SourceNS,
SrcN: v.SourceName,
DstNS: v.DestinationNS,
DstN: v.DestinationName,
})
}
assert.Equal(t, tc.Expected, actual)
})
}
}
func TestIntention_SetHash(t *testing.T) {
i := Intention{
ID: "the-id",
Description: "the-description",
SourceNS: "source-ns",
SourceName: "source-name",
DestinationNS: "dest-ns",
DestinationName: "dest-name",
SourceType: "source-type",
Action: "action",
Precedence: 123,
Meta: map[string]string{
"meta1": "one",
"meta2": "two",
},
}
i.SetHash()
expected := []byte{
0x20, 0x89, 0x55, 0xdb, 0x69, 0x34, 0xce, 0x89, 0xd8, 0xb9, 0x2e, 0x3a,
0x85, 0xb6, 0xea, 0x43, 0xb2, 0x23, 0x16, 0x93, 0x94, 0x13, 0x2a, 0xe4,
0x81, 0xfe, 0xe, 0x34, 0x91, 0x99, 0xe9, 0x8d,
}
require.Equal(t, expected, i.Hash)
}
func TestIntention_String(t *testing.T) {
type testcase struct {
ixn *Intention
expect string
}
testID := generateUUID()
partitionPrefix := DefaultEnterpriseMetaInDefaultPartition().PartitionOrEmpty()
if partitionPrefix != "" {
partitionPrefix += "/"
}
cases := map[string]testcase{
"legacy allow": {
&Intention{
ID: testID,
SourceName: "foo",
DestinationName: "bar",
Action: IntentionActionAllow,
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (ID: ` + testID + `, Precedence: 9, Action: ALLOW)`,
},
"legacy deny": {
&Intention{
ID: testID,
SourceName: "foo",
DestinationName: "bar",
Action: IntentionActionDeny,
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (ID: ` + testID + `, Precedence: 9, Action: DENY)`,
},
"L4 allow": {
&Intention{
SourceName: "foo",
DestinationName: "bar",
Action: IntentionActionAllow,
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
},
"L4 deny": {
&Intention{
SourceName: "foo",
DestinationName: "bar",
Action: IntentionActionDeny,
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: DENY)`,
},
"L7 one perm": {
&Intention{
SourceName: "foo",
DestinationName: "bar",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathPrefix: "/foo",
},
},
},
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Permissions: 1)`,
},
"L7 two perms": {
&Intention{
SourceName: "foo",
DestinationName: "bar",
Permissions: []*IntentionPermission{
{
Action: IntentionActionDeny,
HTTP: &IntentionHTTPPermission{
PathExact: "/foo/admin",
},
},
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
PathPrefix: "/foo",
},
},
},
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Permissions: 2)`,
},
"L4 allow with source peer": {
&Intention{
SourceName: "foo",
SourcePeer: "billing",
DestinationName: "bar",
Action: IntentionActionAllow,
},
`peer(billing)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
},
"L4 allow with source sameness group": {
&Intention{
SourceName: "foo",
SourceSamenessGroup: "group-1",
DestinationName: "bar",
Action: IntentionActionAllow,
},
`sameness-group(group-1)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
},
}
for name, tc := range cases {
tc := tc
// Add a bunch of required fields.
tc.ixn.FillPartitionAndNamespace(DefaultEnterpriseMetaInDefaultPartition(), true)
tc.ixn.UpdatePrecedence()
t.Run(name, func(t *testing.T) {
got := tc.ixn.String()
require.Equal(t, tc.expect, got)
})
}
}
func TestIntentionQueryRequest_CacheInfoKey(t *testing.T) {
assertCacheInfoKeyIsComplete(t, &IntentionQueryRequest{})
}