consul/agent/xds/rbac_test.go

1467 lines
48 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xds
import (
"fmt"
"path/filepath"
"regexp"
"sort"
"testing"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
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/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/xdsv2"
"github.com/hashicorp/consul/proto-public/pbmesh/v2beta1/pbproxystate"
"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.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.SimplifiedIntentions(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.SimplifiedIntentions
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, err := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle, nil)
intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow)
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo)
require.NoError(t, err)
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
}
testIntentionWithJWT := func(src string, action structs.IntentionAction, jwt *structs.IntentionJWTRequirement, perms ...*structs.IntentionPermission) *structs.Intention {
ixn := testIntention(t, src, "api", action)
ixn.JWT = jwt
ixn.Action = action
if perms != nil {
ixn.Permissions = perms
ixn.Action = ""
}
return ixn
}
testPeerTrustBundle := []*pbpeering.PeeringTrustBundle{
{
PeerName: "peer1",
TrustDomain: "peer1.domain",
ExportedPartition: "part1",
},
}
testTrustDomain := "test.consul"
sorted := func(ixns ...*structs.Intention) structs.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.SimplifiedIntentions(ixns)
}
var (
permSlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
oktaWithClaims = structs.IntentionJWTProvider{
Name: "okta",
VerifyClaims: []*structs.IntentionJWTClaimVerification{
{Path: []string{"roles"}, Value: "testing"},
},
}
auth0WithClaims = structs.IntentionJWTProvider{
Name: "auth0",
VerifyClaims: []*structs.IntentionJWTClaimVerification{
{Path: []string{"perms", "role"}, Value: "admin"},
},
}
testJWTProviderConfigEntry = map[string]*structs.JWTProviderConfigEntry{
"okta": {Name: "okta", Issuer: "mytest.okta-issuer"},
"auth0": {Name: "auth0", Issuer: "mytest.auth0-issuer"},
}
jwtRequirement = &structs.IntentionJWTRequirement{
Providers: []*structs.IntentionJWTProvider{
&oktaWithClaims,
},
}
auth0Requirement = &structs.IntentionJWTRequirement{
Providers: []*structs.IntentionJWTProvider{
&auth0WithClaims,
},
}
permDenySlashPrefix = &structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
}
)
makeSpiffe := func(name string, entMeta *acl.EnterpriseMeta) *pbproxystate.Spiffe {
em := *acl.DefaultEnterpriseMeta()
if entMeta != nil {
em = *entMeta
}
regex := makeSpiffePattern(rbacService{
ServiceName: structs.ServiceName{
Name: name,
EnterpriseMeta: em,
},
TrustDomain: testTrustDomain,
})
return &pbproxystate.Spiffe{Regex: regex}
}
tests := map[string]struct {
intentionDefaultAllow bool
v1Intentions structs.SimplifiedIntentions
v2L4TrafficPermissions *pbproxystate.TrafficPermissions
}{
"default-deny-mixed-precedence": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testIntention(t, "web", "api", structs.IntentionActionAllow),
testIntention(t, "*", "api", structs.IntentionActionDeny),
testIntention(t, "web", "*", structs.IntentionActionDeny),
),
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
},
},
},
},
"default-deny-service-wildcard-allow": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourceIntention("*", structs.IntentionActionAllow),
),
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("*", nil),
},
},
},
},
},
},
"default-allow-service-wildcard-deny": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-one-allow": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourceIntention("web", structs.IntentionActionAllow),
),
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
},
},
},
},
"default-allow-one-deny": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourceIntention("web", structs.IntentionActionDeny),
),
},
"default-deny-allow-deny": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourceIntention("web", structs.IntentionActionDeny),
testSourceIntention("*", structs.IntentionActionAllow),
),
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("*", nil),
ExcludeSpiffes: []*pbproxystate.Spiffe{makeSpiffe("web", nil)},
},
},
},
},
},
},
"default-deny-kitchen-sink": {
intentionDefaultAllow: false,
v1Intentions: sorted(
// (double exact)
testSourceIntention("web", structs.IntentionActionAllow),
testSourceIntention("unsafe", structs.IntentionActionDeny),
testSourceIntention("cron", structs.IntentionActionAllow),
testSourceIntention("*", structs.IntentionActionAllow),
),
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("cron", nil),
},
{
Spiffe: makeSpiffe("web", nil),
},
{
Spiffe: makeSpiffe("*", nil),
ExcludeSpiffes: []*pbproxystate.Spiffe{
makeSpiffe("web", nil),
makeSpiffe("unsafe", nil),
makeSpiffe("cron", nil),
},
},
},
},
},
},
},
"v2-kitchen-sink": {
intentionDefaultAllow: false,
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("api", nil),
},
{
Spiffe: makeSpiffe("*", nil),
ExcludeSpiffes: []*pbproxystate.Spiffe{
makeSpiffe("unsafe", nil),
},
},
},
},
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
},
},
DenyPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("db", nil),
},
{
Spiffe: makeSpiffe("cron", nil),
},
},
},
},
},
},
"v2-default-deny": {
intentionDefaultAllow: false,
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{},
},
"v2-default-allow": {
intentionDefaultAllow: true,
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{},
},
// This validates that we don't send xDS messages to Envoy that will fail validation.
// Traffic permissions validations prevent this from being written to the IR, so the thing
// that matters is that the snapshot is valid to Envoy.
"v2-ignore-empty-permissions": {
intentionDefaultAllow: false,
v2L4TrafficPermissions: &pbproxystate.TrafficPermissions{
DenyPermissions: []*pbproxystate.Permission{
{},
},
AllowPermissions: []*pbproxystate.Permission{
{},
},
},
},
"default-allow-kitchen-sink": {
intentionDefaultAllow: true,
v1Intentions: 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,
v1Intentions: sorted(
testSourceIntention("web", structs.IntentionActionAllow),
testIntentionPeered("*", "peer1", structs.IntentionActionAllow),
testIntentionPeered("web", "peer1", structs.IntentionActionDeny),
),
},
// ========================
"default-allow-path-allow": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
},
"default-deny-path-allow": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
},
"default-allow-path-deny": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
},
"default-deny-path-deny": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
},
// ========================
"default-allow-deny-all-and-path-allow": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-deny-all-and-path-allow": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-allow-deny-all-and-path-deny": {
intentionDefaultAllow: true,
v1Intentions: sorted(
testSourcePermIntention("web",
&structs.IntentionPermission{
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "/",
},
},
),
testSourceIntention("*", structs.IntentionActionDeny),
),
},
"default-deny-deny-all-and-path-deny": {
intentionDefaultAllow: false,
v1Intentions: 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,
v1Intentions: 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,
v1Intentions: 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,
v1Intentions: 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,
v1Intentions: 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},
},
},
},
),
),
},
// ========= JWTAuthn Filter checks
"top-level-jwt-no-permissions": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testIntentionWithJWT("web", structs.IntentionActionAllow, jwtRequirement),
),
},
"empty-top-level-jwt-with-one-permission": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testIntentionWithJWT("web", structs.IntentionActionAllow, nil, &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "some-path",
},
JWT: jwtRequirement,
}),
),
},
"top-level-jwt-with-one-permission": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testIntentionWithJWT("web",
structs.IntentionActionAllow,
jwtRequirement,
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
JWT: auth0Requirement,
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/admin",
},
},
),
),
},
"top-level-jwt-with-multiple-permissions": {
intentionDefaultAllow: false,
v1Intentions: sorted(
testIntentionWithJWT("web",
structs.IntentionActionAllow,
jwtRequirement,
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/secret",
},
JWT: auth0Requirement,
},
&structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/v1/admin",
},
JWT: auth0Requirement,
},
),
),
},
}
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) {
t.Run("current", func(t *testing.T) {
if len(tt.v1Intentions) == 0 {
return
}
filter, err := makeRBACNetworkFilter(tt.v1Intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle)
require.NoError(t, err)
gotJSON := protoToJSON(t, filter)
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name), gotJSON), gotJSON)
})
t.Run("v1 vs v2", func(t *testing.T) {
if tt.v2L4TrafficPermissions == nil {
return
}
tt.v2L4TrafficPermissions.DefaultAllow = tt.intentionDefaultAllow
filters, err := xdsv2.MakeRBACNetworkFilters(tt.v2L4TrafficPermissions)
require.NoError(t, err)
var gotJSON string
if len(filters) == 1 {
gotJSON = protoToJSON(t, filters[0])
} else {
// This is wrapped because protoToJSON won't encode an array of protobufs.
chain := &envoy_listener_v3.FilterChain{}
chain.Filters = filters
gotJSON = protoToJSON(t, chain)
}
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name), gotJSON), gotJSON)
})
})
t.Run("http filter", func(t *testing.T) {
if len(tt.v1Intentions) == 0 {
return
}
filter, err := makeRBACHTTPFilter(tt.v1Intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle, testJWTProviderConfigEntry)
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.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence
})
return structs.SimplifiedIntentions(ixns)
}
tests := map[string]struct {
in structs.SimplifiedIntentions
expect structs.SimplifiedIntentions
}{
"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])
})
}
}
func TestPathToSegments(t *testing.T) {
tests := map[string]struct {
key string
paths []string
expected []*envoy_matcher_v3.MetadataMatcher_PathSegment
}{
"single-path": {
key: "jwt_payload_okta",
paths: []string{"perms"},
expected: []*envoy_matcher_v3.MetadataMatcher_PathSegment{
{
Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "jwt_payload_okta"},
},
{
Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "perms"},
},
},
},
"multi-paths": {
key: "jwt_payload_okta",
paths: []string{"perms", "roles"},
expected: []*envoy_matcher_v3.MetadataMatcher_PathSegment{
{
Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "jwt_payload_okta"},
},
{
Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "perms"},
},
{
Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "roles"},
},
},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
segments := pathToSegments(tt.paths, tt.key)
require.ElementsMatch(t, segments, tt.expected)
})
}
}
func TestJWTClaimsToPrincipals(t *testing.T) {
var (
firstClaim = structs.IntentionJWTClaimVerification{
Path: []string{"perms"},
Value: "admin",
}
secondClaim = structs.IntentionJWTClaimVerification{
Path: []string{"passage"},
Value: "secret",
}
payloadKey = "dummy-key"
firstPrincipal = envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{
Filter: jwtEnvoyFilter,
Path: pathToSegments(firstClaim.Path, payloadKey),
Value: &envoy_matcher_v3.ValueMatcher{
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: firstClaim.Value,
},
},
},
},
},
},
}
secondPrincipal = envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{
Filter: jwtEnvoyFilter,
Path: pathToSegments(secondClaim.Path, payloadKey),
Value: &envoy_matcher_v3.ValueMatcher{
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: secondClaim.Value,
},
},
},
},
},
},
}
)
tests := map[string]struct {
claims []*structs.IntentionJWTClaimVerification
metadataPayloadKey string
expected *envoy_rbac_v3.Principal
}{
"single-claim": {
claims: []*structs.IntentionJWTClaimVerification{&firstClaim},
metadataPayloadKey: payloadKey,
expected: &firstPrincipal,
},
"multiple-claims": {
claims: []*structs.IntentionJWTClaimVerification{&firstClaim, &secondClaim},
metadataPayloadKey: payloadKey,
expected: &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_AndIds{
AndIds: &envoy_rbac_v3.Principal_Set{
Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal},
},
},
},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
principal := jwtClaimsToPrincipals(tt.claims, tt.metadataPayloadKey)
require.Equal(t, principal, tt.expected)
})
}
}