mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
535 lines
15 KiB
535 lines
15 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package structs |
|
|
|
import ( |
|
"sort" |
|
"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)) |
|
}) |
|
} |
|
} |
|
|
|
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", |
|
}, |
|
} |
|
|
|
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) |
|
if err == nil { |
|
return |
|
} |
|
|
|
assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) |
|
}) |
|
} |
|
} |
|
|
|
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{}) |
|
}
|
|
|