open-consul/agent/xds/rbac_test.go
Matt Keeler f3c80c4eef
Protobuf Refactoring for Multi-Module Cleanliness (#16302)
Protobuf Refactoring for Multi-Module Cleanliness

This commit includes the following:

Moves all packages that were within proto/ to proto/private
Rewrites imports to account for the packages being moved
Adds in buf.work.yaml to enable buf workspaces
Names the proto-public buf module so that we can override the Go package imports within proto/buf.yaml
Bumps the buf version dependency to 1.14.0 (I was trying out the version to see if it would get around an issue - it didn't but it also doesn't break things and it seemed best to keep up with the toolchain changes)

Why:

In the future we will need to consume other protobuf dependencies such as the Google HTTP annotations for openapi generation or grpc-gateway usage.
There were some recent changes to have our own ratelimiting annotations.
The two combined were not working when I was trying to use them together (attempting to rebase another branch)
Buf workspaces should be the solution to the problem
Buf workspaces means that each module will have generated Go code that embeds proto file names relative to the proto dir and not the top level repo root.
This resulted in proto file name conflicts in the Go global protobuf type registry.
The solution to that was to add in a private/ directory into the path within the proto/ directory.
That then required rewriting all the imports.

Is this safe?

AFAICT yes
The gRPC wire protocol doesn't seem to care about the proto file names (although the Go grpc code does tack on the proto file name as Metadata in the ServiceDesc)
Other than imports, there were no changes to any generated code as a result of this.
2023-02-17 16:14:46 -05:00

1060 lines
36 KiB
Go

package xds
import (
"fmt"
"path/filepath"
"regexp"
"sort"
"testing"
envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbpeering"
)
func TestRemoveIntentionPrecedence(t *testing.T) {
type ixnOpts struct {
src string
peer string
action structs.IntentionAction
}
testIntention := func(t *testing.T, opts ixnOpts) *structs.Intention {
t.Helper()
ixn := structs.TestIntention(t)
ixn.SourceName = opts.src
ixn.SourcePeer = opts.peer
ixn.Action = opts.action
// Destination is hardcoded, since RBAC rules are generated for a single destination
ixn.DestinationName = "api"
//nolint:staticcheck
ixn.UpdatePrecedence()
return ixn
}
testSourceIntention := func(opts ixnOpts) *structs.Intention {
return testIntention(t, opts)
}
testSourcePermIntention := func(src string, perms ...*structs.IntentionPermission) *structs.Intention {
opts := ixnOpts{src: src}
ixn := testIntention(t, opts)
ixn.Permissions = perms
return ixn
}
sorted := func(ixns ...*structs.Intention) structs.Intentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.Intentions(ixns)
}
testPeerTrustBundle := map[string]*pbpeering.PeeringTrustBundle{
"peer1": {
PeerName: "peer1",
TrustDomain: "peer1.domain",
ExportedPartition: "part1",
},
}
testTrustDomain := "test.consul"
var (
nameWild = rbacService{ServiceName: structs.NewServiceName("*", nil),
TrustDomain: testTrustDomain}
nameWeb = rbacService{ServiceName: structs.NewServiceName("web", nil),
TrustDomain: testTrustDomain}
nameWildPeered = rbacService{ServiceName: structs.NewServiceName("*", nil),
Peer: "peer1", TrustDomain: "peer1.domain", ExportedPartition: "part1"}
nameWebPeered = rbacService{ServiceName: structs.NewServiceName("web", nil),
Peer: "peer1", TrustDomain: "peer1.domain", ExportedPartition: "part1"}
permSlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
permDenySlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
xdsPermSlashPrefix = &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_UrlPath{
UrlPath: &envoy_matcher_v3.PathMatcher{
Rule: &envoy_matcher_v3.PathMatcher_Path{
Path: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: "/",
},
},
},
},
},
}
)
// NOTE: these default=(allow|deny) wild=(allow|deny) path=(allow|deny)
// tests below are meant to verify some of the behaviors work as expected
// when the default acl mode changes for the system
tests := map[string]struct {
intentionDefaultAllow bool
http bool
intentions structs.Intentions
expect []*rbacIntention
}{
"default-allow-path-allow": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
expect: []*rbacIntention{}, // EMPTY, just use the defaults
},
"default-deny-path-allow": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permSlashPrefix,
Action: intentionActionAllow,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
},
},
"default-allow-path-deny": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permDenySlashPrefix,
Action: intentionActionDeny,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
},
},
"default-deny-path-deny": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
expect: []*rbacIntention{},
},
// ========================
"default-allow-deny-all-and-path-allow": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionDeny}),
),
expect: []*rbacIntention{
{
Source: nameWild,
NotSources: []rbacService{
nameWeb,
},
Action: intentionActionDeny,
Permissions: nil,
Precedence: 8,
Skip: false,
ComputedPrincipal: andPrincipals(
[]*envoy_rbac_v3.Principal{
idPrincipal(nameWild),
notPrincipal(
idPrincipal(nameWeb),
),
},
),
},
},
},
"default-deny-deny-all-and-path-allow": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionDeny}),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permSlashPrefix,
Action: intentionActionAllow,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
},
},
"default-allow-deny-all-and-path-deny": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionDeny}),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permDenySlashPrefix,
Action: intentionActionDeny,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
{
Source: nameWild,
NotSources: []rbacService{
nameWeb,
},
Action: intentionActionDeny,
Permissions: nil,
Precedence: 8,
Skip: false,
ComputedPrincipal: andPrincipals(
[]*envoy_rbac_v3.Principal{
idPrincipal(nameWild),
notPrincipal(
idPrincipal(nameWeb),
),
},
),
},
},
},
"default-deny-deny-all-and-path-deny": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionDeny}),
),
expect: []*rbacIntention{},
},
// ========================
"default-allow-allow-all-and-path-allow": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionAllow}),
),
expect: []*rbacIntention{},
},
"default-deny-allow-all-and-path-allow": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionAllow}),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permSlashPrefix,
Action: intentionActionAllow,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
{
Source: nameWild,
NotSources: []rbacService{
nameWeb,
},
Action: intentionActionAllow,
Permissions: nil,
Precedence: 8,
Skip: false,
ComputedPrincipal: andPrincipals(
[]*envoy_rbac_v3.Principal{
idPrincipal(nameWild),
notPrincipal(
idPrincipal(nameWeb),
),
},
),
},
},
},
"default-allow-allow-all-and-path-deny": {
intentionDefaultAllow: true,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionAllow}),
),
expect: []*rbacIntention{
{
Source: nameWeb,
Action: intentionActionLayer7,
Permissions: []*rbacPermission{
{
Definition: permDenySlashPrefix,
Action: intentionActionDeny,
Perm: xdsPermSlashPrefix,
NotPerms: nil,
Skip: false,
ComputedPermission: xdsPermSlashPrefix,
},
},
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWeb),
},
},
},
"default-deny-allow-all-and-path-deny": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
testSourceIntention(ixnOpts{src: "*", action: structs.IntentionActionAllow}),
),
expect: []*rbacIntention{
{
Source: nameWild,
NotSources: []rbacService{
nameWeb,
},
Action: intentionActionAllow,
Permissions: nil,
Precedence: 8,
Skip: false,
ComputedPrincipal: andPrincipals(
[]*envoy_rbac_v3.Principal{
idPrincipal(nameWild),
notPrincipal(
idPrincipal(nameWeb),
),
},
),
},
},
},
// ========= Sanity check that peers get passed through
"default-deny-peered": {
intentionDefaultAllow: false,
http: true,
intentions: sorted(
testSourceIntention(ixnOpts{
src: "*",
action: structs.IntentionActionAllow,
peer: "peer1",
}),
testSourceIntention(ixnOpts{
src: "web",
action: structs.IntentionActionAllow,
peer: "peer1",
}),
),
expect: []*rbacIntention{
{
Source: nameWebPeered,
Action: intentionActionAllow,
Permissions: nil,
Precedence: 9,
Skip: false,
ComputedPrincipal: idPrincipal(nameWebPeered),
},
{
Source: nameWildPeered,
Action: intentionActionAllow,
NotSources: []rbacService{
nameWebPeered,
},
Permissions: nil,
Precedence: 8,
Skip: false,
ComputedPrincipal: andPrincipals(
[]*envoy_rbac_v3.Principal{
idPrincipal(nameWildPeered),
notPrincipal(
idPrincipal(nameWebPeered),
),
},
),
},
},
},
}
testLocalInfo := rbacLocalInfo{
trustDomain: testTrustDomain,
datacenter: "dc1",
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
rbacIxns := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle)
intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow)
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo)
require.Equal(t, tt.expect, rbacIxns)
})
}
}
func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
testIntention := func(t *testing.T, src, dst string, action structs.IntentionAction) *structs.Intention {
t.Helper()
ixn := structs.TestIntention(t)
ixn.SourceName = src
ixn.DestinationName = dst
ixn.Action = action
//nolint:staticcheck
ixn.UpdatePrecedence()
return ixn
}
testSourceIntention := func(src string, action structs.IntentionAction) *structs.Intention {
return testIntention(t, src, "api", action)
}
testIntentionPeered := func(src string, peer string, action structs.IntentionAction) *structs.Intention {
ixn := testIntention(t, src, "api", action)
ixn.SourcePeer = peer
return ixn
}
testSourcePermIntention := func(src string, perms ...*structs.IntentionPermission) *structs.Intention {
ixn := testIntention(t, src, "api", "")
ixn.Permissions = perms
return ixn
}
testPeerTrustBundle := []*pbpeering.PeeringTrustBundle{
{
PeerName: "peer1",
TrustDomain: "peer1.domain",
ExportedPartition: "part1",
},
}
testTrustDomain := "test.consul"
sorted := func(ixns ...*structs.Intention) structs.Intentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.Intentions(ixns)
}
var (
permSlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
permDenySlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
)
tests := map[string]struct {
intentionDefaultAllow bool
intentions structs.Intentions
}{
"default-deny-mixed-precedence": {
intentionDefaultAllow: false,
intentions: sorted(
testIntention(t, "web", "api", structs.IntentionActionAllow),
testIntention(t, "*", "api", structs.IntentionActionDeny),
testIntention(t, "web", "*", structs.IntentionActionDeny),
),
},
"default-deny-service-wildcard-allow": {
intentionDefaultAllow: false,
intentions: sorted(
testSourceIntention("*", structs.IntentionActionAllow),
),
},
"default-allow-service-wildcard-deny": {
intentionDefaultAllow: true,
intentions: sorted(
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-one-allow": {
intentionDefaultAllow: false,
intentions: sorted(
testSourceIntention("web", structs.IntentionActionAllow),
),
},
"default-allow-one-deny": {
intentionDefaultAllow: true,
intentions: sorted(
testSourceIntention("web", structs.IntentionActionDeny),
),
},
"default-deny-allow-deny": {
intentionDefaultAllow: false,
intentions: sorted(
testSourceIntention("web", structs.IntentionActionDeny),
testSourceIntention("*", structs.IntentionActionAllow),
),
},
"default-deny-kitchen-sink": {
intentionDefaultAllow: false,
intentions: sorted(
// (double exact)
testSourceIntention("web", structs.IntentionActionAllow),
testSourceIntention("unsafe", structs.IntentionActionDeny),
testSourceIntention("cron", structs.IntentionActionAllow),
testSourceIntention("*", structs.IntentionActionAllow),
),
},
"default-allow-kitchen-sink": {
intentionDefaultAllow: true,
intentions: sorted(
// (double exact)
testSourceIntention("web", structs.IntentionActionDeny),
testSourceIntention("unsafe", structs.IntentionActionAllow),
testSourceIntention("cron", structs.IntentionActionDeny),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-peered-kitchen-sink": {
intentionDefaultAllow: false,
intentions: sorted(
testSourceIntention("web", structs.IntentionActionAllow),
testIntentionPeered("*", "peer1", structs.IntentionActionAllow),
testIntentionPeered("web", "peer1", structs.IntentionActionDeny),
),
},
// ========================
"default-allow-path-allow": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
},
"default-deny-path-allow": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
},
"default-allow-path-deny": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
},
"default-deny-path-deny": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
},
// ========================
"default-allow-deny-all-and-path-allow": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-deny-all-and-path-allow": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-allow-deny-all-and-path-deny": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-deny-all-and-path-deny": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
// ========================
"default-deny-two-path-deny-and-path-allow": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/admin",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
),
},
"default-allow-two-path-deny-and-path-allow": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/admin",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
),
},
"default-deny-single-intention-with-kitchen-sink-perms": {
intentionDefaultAllow: false,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/v1",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathRegex: "/v[123]",
Methods: []string{"GET", "HEAD", "OPTIONS"},
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
Header: []structs.IntentionHTTPHeaderPermission{
{Name: "x-foo", Present: true},
{Name: "x-bar", Exact: "xyz"},
{Name: "x-dib", Prefix: "gaz"},
{Name: "x-gir", Suffix: "zim"},
{Name: "x-zim", Regex: "gi[rR]"},
{Name: "z-foo", Present: true, Invert: true},
{Name: "z-bar", Exact: "xyz", Invert: true},
{Name: "z-dib", Prefix: "gaz", Invert: true},
{Name: "z-gir", Suffix: "zim", Invert: true},
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
},
},
},
),
),
},
"default-allow-single-intention-with-kitchen-sink-perms": {
intentionDefaultAllow: true,
intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/v1",
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathRegex: "/v[123]",
Methods: []string{"GET", "HEAD", "OPTIONS"},
},
},
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
Header: []structs.IntentionHTTPHeaderPermission{
{Name: "x-foo", Present: true},
{Name: "x-bar", Exact: "xyz"},
{Name: "x-dib", Prefix: "gaz"},
{Name: "x-gir", Suffix: "zim"},
{Name: "x-zim", Regex: "gi[rR]"},
{Name: "z-foo", Present: true, Invert: true},
{Name: "z-bar", Exact: "xyz", Invert: true},
{Name: "z-dib", Prefix: "gaz", Invert: true},
{Name: "z-gir", Suffix: "zim", Invert: true},
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
},
},
},
),
),
},
}
testLocalInfo := rbacLocalInfo{
trustDomain: testTrustDomain,
datacenter: "dc1",
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Run("network filter", func(t *testing.T) {
filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle)
require.NoError(t, err)
t.Run("current", func(t *testing.T) {
gotJSON := protoToJSON(t, filter)
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name), gotJSON), gotJSON)
})
})
t.Run("http filter", func(t *testing.T) {
filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle)
require.NoError(t, err)
t.Run("current", func(t *testing.T) {
gotJSON := protoToJSON(t, filter)
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name+"--httpfilter"), gotJSON), gotJSON)
})
})
})
}
}
func TestRemoveSameSourceIntentions(t *testing.T) {
testIntention := func(t *testing.T, src, dst string) *structs.Intention {
t.Helper()
ixn := structs.TestIntention(t)
ixn.SourceName = src
ixn.DestinationName = dst
//nolint:staticcheck
ixn.UpdatePrecedence()
return ixn
}
testIntentionPeered := func(t *testing.T, src, dst, peer string) *structs.Intention {
t.Helper()
ixn := structs.TestIntention(t)
ixn.SourceName = src
ixn.SourcePeer = peer
ixn.DestinationName = dst
//nolint:staticcheck
ixn.UpdatePrecedence()
return ixn
}
sorted := func(ixns ...*structs.Intention) structs.Intentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.Intentions(ixns)
}
tests := map[string]struct {
in structs.Intentions
expect structs.Intentions
}{
"empty": {},
"one": {
in: sorted(
testIntention(t, "*", "*"),
),
expect: sorted(
testIntention(t, "*", "*"),
),
},
"two with no match": {
in: sorted(
testIntention(t, "*", "foo"),
testIntention(t, "bar", "*"),
),
expect: sorted(
testIntention(t, "*", "foo"),
testIntention(t, "bar", "*"),
),
},
"two with match, exact": {
in: sorted(
testIntention(t, "bar", "foo"),
testIntention(t, "bar", "*"),
),
expect: sorted(
testIntention(t, "bar", "foo"),
),
},
"two with match, wildcard": {
in: sorted(
testIntention(t, "*", "foo"),
testIntention(t, "*", "*"),
),
expect: sorted(
testIntention(t, "*", "foo"),
),
},
"kitchen sink with peers": {
in: sorted(
testIntention(t, "bar", "foo"),
testIntentionPeered(t, "bar", "foo", "peer1"),
testIntentionPeered(t, "bar", "*", "peer1"),
testIntentionPeered(t, "*", "foo", "peer1"),
testIntentionPeered(t, "*", "*", "peer1"),
),
expect: sorted(
testIntention(t, "bar", "foo"),
testIntentionPeered(t, "bar", "foo", "peer1"),
testIntentionPeered(t, "*", "foo", "peer1"),
),
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
got := removeSameSourceIntentions(tc.in)
require.Equal(t, tc.expect, got)
})
}
}
func TestSimplifyNotSourceSlice(t *testing.T) {
tests := map[string]struct {
in []string
expect []string
}{
"empty": {},
"one": {
[]string{"bar"},
[]string{"bar"},
},
"two with no match": {
[]string{"foo", "bar"},
[]string{"foo", "bar"},
},
"two with match": {
[]string{"*", "bar"},
[]string{"*"},
},
"three with two matches down to one": {
[]string{"*", "foo", "bar"},
[]string{"*"},
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
got := simplifyNotSourceSlice(makeServiceNameSlice(tc.in))
require.Equal(t, makeServiceNameSlice(tc.expect), got)
})
}
}
func TestIxnSourceMatches(t *testing.T) {
tests := []struct {
tester string
testerPeer string
against string
againstPeer string
matches bool
}{
// identical precedence
{"web", "", "api", "", false},
{"*", "", "*", "", false},
// backwards precedence
{"*", "", "web", "", false},
// name wildcards
{"web", "", "*", "", true},
// peered cmp peered
{"web", "peer1", "api", "peer1", false},
{"*", "peer1", "*", "peer1", false},
// no match if peer is different
{"web", "peer1", "web", "", false},
{"*", "peer1", "*", "peer2", false},
// name wildcards with peer
{"web", "peer1", "*", "peer1", true},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%s%s cmp %s%s", tc.testerPeer, tc.tester, tc.againstPeer, tc.against), func(t *testing.T) {
matches := ixnSourceMatches(
rbacService{ServiceName: structs.ServiceNameFromString(tc.tester), Peer: tc.testerPeer},
rbacService{ServiceName: structs.ServiceNameFromString(tc.against), Peer: tc.againstPeer},
)
assert.Equal(t, tc.matches, matches)
})
}
}
func makeServiceNameSlice(slice []string) []rbacService {
if len(slice) == 0 {
return nil
}
var out []rbacService
for _, src := range slice {
out = append(out, rbacService{ServiceName: structs.ServiceNameFromString(src)})
}
return out
}
func TestSpiffeMatcher(t *testing.T) {
cases := map[string]struct {
xfcc string
trustDomain string
namespace string
partition string
datacenter string
service string
}{
"between admin partitions": {
xfcc: `By=spiffe://70c72965-291c-d138-e5a6-cfd8a66b395e.consul/ap/ap1/ns/default/dc/primary/svc/s2;Hash=377330adafa619abe52672246b7be7410d74b7497e9d88a8396d641fd6f82ad2;Cert="-----BEGIN%20CERTIFICATE-----%0AMIICGTCCAb%2BgAwIBAgIBCzAKBggqhkjOPQQDAjAwMS4wLAYDVQQDEyVwcmktMTJj%0AOWtvbS5jb25zdWwuY2EuNzBjNzI5NjUuY29uc3VsMB4XDTIyMTIyMjE0MjE1NVoX%0ADTIyMTIyNTE0MjE1NVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPuJbVdQ%0AYsT8RnvMLT%2FpsuZwltWbCkwxzBR03%2FEC4f7TyLy1Mfe6gm%2Fz5K8Tc29d7W16PBT0%0AR%2B1XPfpigopVanyjgfkwgfYwDgYDVR0PAQH%2FBAQDAgO4MB0GA1UdJQQWMBQGCCsG%0AAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCBBxpy1QXfp%0AS4V8QFH%2BEfF39VP51Qbhlj75N5gbUSxGajArBgNVHSMEJDAigCCjWP%2BlGhzd4jbD%0A2QI66cvAAgIkLqG0lz0PyzTz76QoOzBfBgNVHREBAf8EVTBThlFzcGlmZmU6Ly83%0AMGM3Mjk2NS0yOTFjLWQxMzgtZTVhNi1jZmQ4YTY2YjM5NWUuY29uc3VsL25zL2Rl%0AZmF1bHQvZGMvcHJpbWFyeS9zdmMvczEwCgYIKoZIzj0EAwIDSAAwRQIhAJxWHplX%0Aqgmd4cRDMllJsCtOmTZ3v%2B6qDnc545tm%2Bg%2FzAiBwWOqqTZ81BtAtzzWpip1XmUFR%0Afv2SYupWQueXYrOjhw%3D%3D%0A-----END%20CERTIFICATE-----%0A";Chain="-----BEGIN%20CERTIFICATE-----%0AMIICGTCCAb%2BgAwIBAgIBCzAKBggqhkjOPQQDAjAwMS4wLAYDVQQDEyVwcmktMTJj%0AOWtvbS5jb25zdWwuY2EuNzBjNzI5NjUuY29uc3VsMB4XDTIyMTIyMjE0MjE1NVoX%0ADTIyMTIyNTE0MjE1NVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPuJbVdQ%0AYsT8RnvMLT%2FpsuZwltWbCkwxzBR03%2FEC4f7TyLy1Mfe6gm%2Fz5K8Tc29d7W16PBT0%0AR%2B1XPfpigopVanyjgfkwgfYwDgYDVR0PAQH%2FBAQDAgO4MB0GA1UdJQQWMBQGCCsG%0AAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCBBxpy1QXfp%0AS4V8QFH%2BEfF39VP51Qbhlj75N5gbUSxGajArBgNVHSMEJDAigCCjWP%2BlGhzd4jbD%0A2QI66cvAAgIkLqG0lz0PyzTz76QoOzBfBgNVHREBAf8EVTBThlFzcGlmZmU6Ly83%0AMGM3Mjk2NS0yOTFjLWQxMzgtZTVhNi1jZmQ4YTY2YjM5NWUuY29uc3VsL25zL2Rl%0AZmF1bHQvZGMvcHJpbWFyeS9zdmMvczEwCgYIKoZIzj0EAwIDSAAwRQIhAJxWHplX%0Aqgmd4cRDMllJsCtOmTZ3v%2B6qDnc545tm%2Bg%2FzAiBwWOqqTZ81BtAtzzWpip1XmUFR%0Afv2SYupWQueXYrOjhw%3D%3D%0A-----END%20CERTIFICATE-----%0A";Subject="";URI=spiffe://70c72965-291c-d138-e5a6-cfd8a66b395e.consul/ap/ap9/ns/default/dc/primary/svc/s1`,
trustDomain: "70c72965-291c-d138-e5a6-cfd8a66b395e.consul",
namespace: "default",
partition: "ap9",
datacenter: "primary",
service: "s1",
},
"between services": {
xfcc: `By=spiffe://f1efe25e-a9b1-1ae1-b580-98000b84a935.consul/ns/default/dc/primary/svc/s2;Hash=c552ee3990fd6e9bb38b1a8bdd28e8358c339d282e6bb92fc86d04915407f47d;Cert="-----BEGIN%20CERTIFICATE-----%0AMIICGjCCAcCgAwIBAgIBCzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktOGFt%0AMjNueXouY29uc3VsLmNhLmYxZWZlMjVlLmNvbnN1bDAeFw0yMjEyMjIxNTIxMDVa%0AFw0yMjEyMjUxNTIxMDVaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASrChLh%0AelrBB5e8X78fSvbKxD8yieadFg4XUeJtZh2xwdWckCGDEtT984ihgM8Hu4E%2FGpgD%0AJcExohFnS4H%2BG3uco4H5MIH2MA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQghpyuV%2F4g%0Ac6x%2B5jC9uOZQMY4Km2YZwAnSmmTydjjn7qwwKwYDVR0jBCQwIoAgdO0jdTJzfKYq%0ARCYrWbHr7q%2Bq66ispOnMs6HsEwlxV%2F8wXwYDVR0RAQH%2FBFUwU4ZRc3BpZmZlOi8v%0AZjFlZmUyNWUtYTliMS0xYWUxLWI1ODAtOTgwMDBiODRhOTM1LmNvbnN1bC9ucy9k%0AZWZhdWx0L2RjL3ByaW1hcnkvc3ZjL3MxMAoGCCqGSM49BAMCA0gAMEUCIQDTNsze%0AXCj16YvFsX0PUeUBcX4Hh0nmIkMOHCQiPkXTiAIgKJKf038s6muFJw9UQJJ5SSg%2F%0A3RL1wIWXRhsqy1Y89JQ%3D%0A-----END%20CERTIFICATE-----%0A";Chain="-----BEGIN%20CERTIFICATE-----%0AMIICGjCCAcCgAwIBAgIBCzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktOGFt%0AMjNueXouY29uc3VsLmNhLmYxZWZlMjVlLmNvbnN1bDAeFw0yMjEyMjIxNTIxMDVa%0AFw0yMjEyMjUxNTIxMDVaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASrChLh%0AelrBB5e8X78fSvbKxD8yieadFg4XUeJtZh2xwdWckCGDEtT984ihgM8Hu4E%2FGpgD%0AJcExohFnS4H%2BG3uco4H5MIH2MA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQghpyuV%2F4g%0Ac6x%2B5jC9uOZQMY4Km2YZwAnSmmTydjjn7qwwKwYDVR0jBCQwIoAgdO0jdTJzfKYq%0ARCYrWbHr7q%2Bq66ispOnMs6HsEwlxV%2F8wXwYDVR0RAQH%2FBFUwU4ZRc3BpZmZlOi8v%0AZjFlZmUyNWUtYTliMS0xYWUxLWI1ODAtOTgwMDBiODRhOTM1LmNvbnN1bC9ucy9k%0AZWZhdWx0L2RjL3ByaW1hcnkvc3ZjL3MxMAoGCCqGSM49BAMCA0gAMEUCIQDTNsze%0AXCj16YvFsX0PUeUBcX4Hh0nmIkMOHCQiPkXTiAIgKJKf038s6muFJw9UQJJ5SSg%2F%0A3RL1wIWXRhsqy1Y89JQ%3D%0A-----END%20CERTIFICATE-----%0A";Subject="";URI=spiffe://f1efe25e-a9b1-1ae1-b580-98000b84a935.consul/ns/default/dc/primary/svc/s1`,
trustDomain: "f1efe25e-a9b1-1ae1-b580-98000b84a935.consul",
namespace: "default",
datacenter: "primary",
service: "s1",
},
"between peers": {
xfcc: `By=spiffe://ca9857da-71aa-c5be-ec8f-abcd90cae693.consul/gateway/mesh/dc/alpha;Hash=419c850ddc7a32edc752d73bb0f0c6e4c2f5b40feae7cf0cdeeb6f3dd759ed1f;Cert="-----BEGIN%20CERTIFICATE-----%0AMIICGzCCAcCgAwIBAgIBCzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktcTgw%0AdmcxMXQuY29uc3VsLmNhLmZjOWEwOGVmLmNvbnN1bDAeFw0yMjEyMjIxNTIyNTBa%0AFw0yMjEyMjUxNTIyNTBaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQnQtQ6%0AFS%2FqjpopxZIaJtYL3pOx%2BgrzoLtKStCS0SUtGbTBmxmTeIX5l5HHD4yqCWk4M1Iv%0AXNflWvKcpw5KS1tLo4H5MIH2MA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQg%2B8FyVm2p%0AdpzfijuCYeByJQH5mUkqY6%2FciCC2yScNusQwKwYDVR0jBCQwIoAgy0MyubT%2BMNQv%0A%2BuZGeBqa1yU9Fx9641epfbY%2BuSs7cbowXwYDVR0RAQH%2FBFUwU4ZRc3BpZmZlOi8v%0AZmM5YTA4ZWYtZWZiNC1iYmM5LWIzZWMtYjkzZTc2OGFiZmMyLmNvbnN1bC9ucy9k%0AZWZhdWx0L2RjL3ByaW1hcnkvc3ZjL3MxMAoGCCqGSM49BAMCA0kAMEYCIQDp7hX0%0AJ%2FjrAP71jDt2w3uKQJnfZ93d%2FRub2t%2FRwQfsVAIhAL4VUbk5XUvBzwabuEfMCf4O%0AT5rjXDbCWYNN2m4xZFtt%0A-----END%20CERTIFICATE-----%0A";Chain="-----BEGIN%20CERTIFICATE-----%0AMIICGzCCAcCgAwIBAgIBCzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktcTgw%0AdmcxMXQuY29uc3VsLmNhLmZjOWEwOGVmLmNvbnN1bDAeFw0yMjEyMjIxNTIyNTBa%0AFw0yMjEyMjUxNTIyNTBaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQnQtQ6%0AFS%2FqjpopxZIaJtYL3pOx%2BgrzoLtKStCS0SUtGbTBmxmTeIX5l5HHD4yqCWk4M1Iv%0AXNflWvKcpw5KS1tLo4H5MIH2MA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQg%2B8FyVm2p%0AdpzfijuCYeByJQH5mUkqY6%2FciCC2yScNusQwKwYDVR0jBCQwIoAgy0MyubT%2BMNQv%0A%2BuZGeBqa1yU9Fx9641epfbY%2BuSs7cbowXwYDVR0RAQH%2FBFUwU4ZRc3BpZmZlOi8v%0AZmM5YTA4ZWYtZWZiNC1iYmM5LWIzZWMtYjkzZTc2OGFiZmMyLmNvbnN1bC9ucy9k%0AZWZhdWx0L2RjL3ByaW1hcnkvc3ZjL3MxMAoGCCqGSM49BAMCA0kAMEYCIQDp7hX0%0AJ%2FjrAP71jDt2w3uKQJnfZ93d%2FRub2t%2FRwQfsVAIhAL4VUbk5XUvBzwabuEfMCf4O%0AT5rjXDbCWYNN2m4xZFtt%0A-----END%20CERTIFICATE-----%0A";Subject="";URI=spiffe://fc9a08ef-efb4-bbc9-b3ec-b93e768abfc2.consul/ns/default/dc/primary/svc/s1,By=spiffe://ca9857da-71aa-c5be-ec8f-abcd90cae693.consul/ns/default/dc/alpha/svc/s2;Hash=1db4ea1e68df1ea0cec7d7ba882ca734d3e1a29a0fe64e73275b6ab796234295;Cert="-----BEGIN%20CERTIFICATE-----%0AMIICEjCCAbmgAwIBAgIBDDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktMXky%0AZXVpbHkuY29uc3VsLmNhLmNhOTg1N2RhLmNvbnN1bDAeFw0yMjEyMjIxNTIzMDVa%0AFw0yMjEyMjUxNTIzMDVaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAROaLaT%0A%2BzyYZKfujWX4vOde%2BnnsGP3z0xaEGQFbgi%2BGU%2BrFfMdadzYF1oXDItS%2FpuBADuha%0Ao0iH2i2aRPUbTm4Ko4HyMIHvMA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQgWTznn%2BPz%0A4eNoiwdO%2FID3uqbyiBJBFbZFAGs7m5KnoCkwKwYDVR0jBCQwIoAgAdVe5N4m4Qlv%0Afgp9tvw0MGq7puWWuLfiw7qghdr1VDIwWAYDVR0RAQH%2FBE4wTIZKc3BpZmZlOi8v%0AY2E5ODU3ZGEtNzFhYS1jNWJlLWVjOGYtYWJjZDkwY2FlNjkzLmNvbnN1bC9nYXRl%0Ad2F5L21lc2gvZGMvYWxwaGEwCgYIKoZIzj0EAwIDRwAwRAIgJu5Z6O10nQe9HAzk%0ARonRMODgENawDHbErpkQ1q91ZTYCIEHccGIEp3OybkvkmIB9s%2Bu%2FbguUjJ4ZKAiD%0AV0dKf1Ao%0A-----END%20CERTIFICATE-----%0A";Chain="-----BEGIN%20CERTIFICATE-----%0AMIICEjCCAbmgAwIBAgIBDDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktMXky%0AZXVpbHkuY29uc3VsLmNhLmNhOTg1N2RhLmNvbnN1bDAeFw0yMjEyMjIxNTIzMDVa%0AFw0yMjEyMjUxNTIzMDVaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAROaLaT%0A%2BzyYZKfujWX4vOde%2BnnsGP3z0xaEGQFbgi%2BGU%2BrFfMdadzYF1oXDItS%2FpuBADuha%0Ao0iH2i2aRPUbTm4Ko4HyMIHvMA4GA1UdDwEB%2FwQEAwIDuDAdBgNVHSUEFjAUBggr%0ABgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH%2FBAIwADApBgNVHQ4EIgQgWTznn%2BPz%0A4eNoiwdO%2FID3uqbyiBJBFbZFAGs7m5KnoCkwKwYDVR0jBCQwIoAgAdVe5N4m4Qlv%0Afgp9tvw0MGq7puWWuLfiw7qghdr1VDIwWAYDVR0RAQH%2FBE4wTIZKc3BpZmZlOi8v%0AY2E5ODU3ZGEtNzFhYS1jNWJlLWVjOGYtYWJjZDkwY2FlNjkzLmNvbnN1bC9nYXRl%0Ad2F5L21lc2gvZGMvYWxwaGEwCgYIKoZIzj0EAwIDRwAwRAIgJu5Z6O10nQe9HAzk%0ARonRMODgENawDHbErpkQ1q91ZTYCIEHccGIEp3OybkvkmIB9s%2Bu%2FbguUjJ4ZKAiD%0AV0dKf1Ao%0A-----END%20CERTIFICATE-----%0A";Subject="";URI=spiffe://ca9857da-71aa-c5be-ec8f-abcd90cae693.consul/gateway/mesh/dc/alpha`,
trustDomain: "fc9a08ef-efb4-bbc9-b3ec-b93e768abfc2.consul",
namespace: "default",
datacenter: "primary",
service: "s1",
},
}
re := regexp.MustCompile(downstreamServiceIdentityMatcher)
for n, c := range cases {
t.Run(n, func(t *testing.T) {
matches := re.FindAllStringSubmatch(c.xfcc, -1)
require.Len(t, matches, 1)
m := matches[0]
require.Equal(t, c.trustDomain, m[1])
require.Equal(t, c.partition, m[2])
require.Equal(t, c.namespace, m[3])
require.Equal(t, c.datacenter, m[4])
require.Equal(t, c.service, m[5])
})
}
}