Browse Source

add traffic permissions excludes and tests (#20453)

* add traffic permissions tests

* review fixes

* Update internal/mesh/internal/controllers/sidecarproxy/builder/local_app.go

Co-authored-by: John Landa <jonathanlanda@gmail.com>

---------

Co-authored-by: John Landa <jonathanlanda@gmail.com>
pull/20523/head
skpratt 10 months ago committed by GitHub
parent
commit
57bad0df85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .github/workflows/test-integrations.yml
  2. 248
      agent/xds/rbac_test.go
  3. 9
      agent/xds/response/response.go
  4. 77
      agent/xds/testdata/rbac/v2-L4-deny-L7-allow--httpfilter.golden
  5. 42
      agent/xds/testdata/rbac/v2-L4-deny-L7-allow.golden
  6. 47
      agent/xds/testdata/rbac/v2-path-excludes--httpfilter.golden
  7. 8
      agent/xds/testdata/rbac/v2-path-excludes.golden
  8. 69
      agent/xds/testdata/rbac/v2-path-method-header-excludes--httpfilter.golden
  9. 8
      agent/xds/testdata/rbac/v2-path-method-header-excludes.golden
  10. 183
      agent/xds/testdata/rbac/v2-single-permission-multiple-destination-rules--httpfilter.golden
  11. 8
      agent/xds/testdata/rbac/v2-single-permission-multiple-destination-rules.golden
  12. 104
      agent/xds/testdata/rbac/v2-single-permission-with-excludes--httpfilter.golden
  13. 8
      agent/xds/testdata/rbac/v2-single-permission-with-excludes.golden
  14. 226
      agent/xds/testdata/rbac/v2-single-permission-with-kitchen-sink-perms--httpfilter.golden
  15. 8
      agent/xds/testdata/rbac/v2-single-permission-with-kitchen-sink-perms.golden
  16. 238
      agent/xdsv2/rbac_resources.go
  17. 14
      internal/auth/internal/types/errors.go
  18. 45
      internal/auth/internal/types/traffic_permissions.go
  19. 3
      internal/auth/internal/types/traffic_permissions_ce_test.go
  20. 107
      internal/auth/internal/types/traffic_permissions_test.go
  21. 95
      internal/mesh/internal/controllers/sidecarproxy/builder/local_app.go
  22. 386
      internal/mesh/internal/controllers/sidecarproxy/builder/local_app_test.go
  23. 63
      proto-public/pbauth/v2beta1/traffic_permission_extras_test.go
  24. 60
      proto-public/pbauth/v2beta1/traffic_permissions_extras.go
  25. 10
      proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.pb.binary.go
  26. 205
      proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.pb.go
  27. 9
      proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.proto
  28. 21
      proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions_deepcopy.gen.go
  29. 11
      proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions_json.gen.go
  30. 459
      test-integ/catalogv2/traffic_permissions_test.go
  31. 54
      test-integ/topoutil/asserter.go
  32. 35
      test/integration/consul-container/libs/assert/envoy.go

1
.github/workflows/test-integrations.yml

@ -536,6 +536,7 @@ jobs:
-tags "${{ env.GOTAGS }}" \
-timeout=20m \
-parallel=2 \
-failfast \
-json \
`go list -tags "${{ env.GOTAGS }}" ./... | grep -v peering_commontopo | grep -v upgrade ` \
--target-image ${{ env.CONSUL_LATEST_IMAGE_NAME }} \

248
agent/xds/rbac_test.go

@ -732,6 +732,58 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
},
},
},
"v2-path-excludes": {
intentionDefaultAllow: false,
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathPrefix: "/admin"},
},
},
},
},
},
DenyPermissions: []*pbproxystate.Permission{},
},
},
"v2-path-method-header-excludes": {
intentionDefaultAllow: false,
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathPrefix: "/admin",
Methods: []string{"POST", "DELETE"},
Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "experiment", Present: true},
},
},
},
},
},
},
},
DenyPermissions: []*pbproxystate.Permission{},
},
},
"v2-default-deny": {
intentionDefaultAllow: false,
v2TrafficPermissions: &pbproxystate.TrafficPermissions{},
@ -784,6 +836,21 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
v1Intentions: sorted(
testSourcePermIntention("web", permSlashPrefix),
),
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{PathPrefix: "/"},
},
},
},
DenyPermissions: []*pbproxystate.Permission{},
},
},
"default-allow-path-deny": {
intentionDefaultAllow: true,
@ -796,6 +863,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
v1Intentions: sorted(
testSourcePermIntention("web", permDenySlashPrefix),
),
v2TrafficPermissions: &pbproxystate.TrafficPermissions{},
},
// ========================
"default-allow-deny-all-and-path-allow": {
@ -869,6 +937,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
),
testSourceIntention("*", structs.IntentionActionDeny),
),
v2TrafficPermissions: &pbproxystate.TrafficPermissions{},
},
// ========================
"default-deny-two-path-deny-and-path-allow": {
@ -895,6 +964,26 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
},
),
),
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/admin"},
{PathExact: "/v1/secret"},
},
},
},
},
},
},
},
"default-allow-two-path-deny-and-path-allow": {
intentionDefaultAllow: true,
@ -964,6 +1053,163 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
),
),
},
"v2-single-permission-with-excludes": {
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/v1",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "x-foo", Present: true},
},
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/secret", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
{PathExact: "/v1/admin", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
},
},
},
},
},
},
},
"v2-single-permission-multiple-destination-rules": {
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/v1",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "x-foo", Present: true},
},
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/secret", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
{PathExact: "/v1/admin", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
},
},
{
PathPrefix: "/v2",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "x-foo", Present: true},
},
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v2/secret", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
{PathExact: "/v2/admin", Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
{Name: "x-bar", Present: true},
}},
},
},
},
},
},
},
},
"v2-single-permission-with-kitchen-sink-perms": {
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/v1",
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/secret"},
},
},
{
PathRegex: "/v[123]",
Methods: []string{"GET", "HEAD", "OPTIONS"},
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/secret"},
{PathExact: "/v1/admin", Methods: []string{"GET"}},
},
},
{
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{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},
},
Exclude: []*pbproxystate.ExcludePermissionRule{
{PathExact: "/v1/secret"},
{Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "x-baz", Present: true},
}},
},
},
},
},
},
},
},
"v2-L4-deny-L7-allow": {
v2TrafficPermissions: &pbproxystate.TrafficPermissions{
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/v1",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "x-foo", Present: true},
},
},
},
},
},
DenyPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: makeSpiffe("web", nil),
},
},
},
},
},
},
"default-allow-single-intention-with-kitchen-sink-perms": {
intentionDefaultAllow: true,
v1Intentions: sorted(
@ -1114,7 +1360,6 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
chain.Filters = filters
gotJSON = protoToJSON(t, chain)
}
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name), gotJSON), gotJSON)
})
})
@ -1152,7 +1397,6 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
manager.HttpFilters = filters
gotJSON = protoToJSON(t, manager)
}
require.JSONEq(t, goldenSimple(t, filepath.Join("rbac", name+"--httpfilter"), gotJSON), gotJSON)
})
})

9
agent/xds/response/response.go

@ -78,12 +78,3 @@ func MakeEnvoyRegexMatch(patt string) *envoy_matcher_v3.RegexMatcher {
Regex: patt,
}
}
func MakeEnvoyStringMatcher(patt string) *envoy_matcher_v3.StringMatcher {
return &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: MakeEnvoyRegexMatch(patt),
},
IgnoreCase: true,
}
}

77
agent/xds/testdata/rbac/v2-L4-deny-L7-allow--httpfilter.golden vendored

@ -0,0 +1,77 @@
{
"httpFilters": [
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"action": "DENY",
"policies": {
"consul-intentions-layer4": {
"permissions": [
{
"any": true
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
},
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/v1"
}
}
},
{
"header": {
"name": "x-foo",
"presentMatch": true
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}
]
}

42
agent/xds/testdata/rbac/v2-L4-deny-L7-allow.golden vendored

@ -0,0 +1,42 @@
{
"filters": [
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {
"action": "DENY",
"policies": {
"consul-intentions-layer4": {
"permissions": [
{
"any": true
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
},
"statPrefix": "connect_authz"
}
},
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}
]
}

47
agent/xds/testdata/rbac/v2-path-excludes--httpfilter.golden vendored

@ -0,0 +1,47 @@
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/"
}
}
},
{
"notRule": {
"urlPath": {
"path": {
"prefix": "/admin"
}
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}

8
agent/xds/testdata/rbac/v2-path-excludes.golden vendored

@ -0,0 +1,8 @@
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}

69
agent/xds/testdata/rbac/v2-path-method-header-excludes--httpfilter.golden vendored

@ -0,0 +1,69 @@
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/"
}
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/admin"
}
}
},
{
"header": {
"name": ":method",
"stringMatch": {
"safeRegex": {
"regex": "POST|DELETE"
}
}
}
},
{
"header": {
"name": "experiment",
"presentMatch": true
}
}
]
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}

8
agent/xds/testdata/rbac/v2-path-method-header-excludes.golden vendored

@ -0,0 +1,8 @@
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}

183
agent/xds/testdata/rbac/v2-single-permission-multiple-destination-rules--httpfilter.golden vendored

@ -0,0 +1,183 @@
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/v1"
}
}
},
{
"header": {
"name": "x-foo",
"presentMatch": true
}
}
]
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v1/admin"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
}
]
}
},
{
"andRules": {
"rules": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/v2"
}
}
},
{
"header": {
"name": "x-foo",
"presentMatch": true
}
}
]
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v2/secret"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v2/admin"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}

8
agent/xds/testdata/rbac/v2-single-permission-multiple-destination-rules.golden vendored

@ -0,0 +1,8 @@
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}

104
agent/xds/testdata/rbac/v2-single-permission-with-excludes--httpfilter.golden vendored

@ -0,0 +1,104 @@
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/v1"
}
}
},
{
"header": {
"name": "x-foo",
"presentMatch": true
}
}
]
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v1/admin"
}
}
},
{
"header": {
"name": "x-baz",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"presentMatch": true
}
}
]
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}

8
agent/xds/testdata/rbac/v2-single-permission-with-excludes.golden vendored

@ -0,0 +1,8 @@
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}

226
agent/xds/testdata/rbac/v2-single-permission-with-kitchen-sink-perms--httpfilter.golden vendored

@ -0,0 +1,226 @@
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"policies": {
"consul-intentions-layer7-0": {
"permissions": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"prefix": "/v1"
}
}
},
{
"notRule": {
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
}
}
]
}
},
{
"andRules": {
"rules": [
{
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"safeRegex": {
"regex": "/v[123]"
}
}
}
},
{
"header": {
"name": ":method",
"stringMatch": {
"safeRegex": {
"regex": "GET|HEAD|OPTIONS"
}
}
}
}
]
}
},
{
"notRule": {
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
}
},
{
"notRule": {
"andRules": {
"rules": [
{
"urlPath": {
"path": {
"exact": "/v1/admin"
}
}
},
{
"header": {
"name": ":method",
"stringMatch": {
"safeRegex": {
"regex": "GET"
}
}
}
}
]
}
}
}
]
}
},
{
"andRules": {
"rules": [
{
"andRules": {
"rules": [
{
"header": {
"name": "x-foo",
"presentMatch": true
}
},
{
"header": {
"name": "x-bar",
"stringMatch": {
"exact": "xyz"
}
}
},
{
"header": {
"name": "x-dib",
"stringMatch": {
"prefix": "gaz"
}
}
},
{
"header": {
"name": "x-gir",
"stringMatch": {
"suffix": "zim"
}
}
},
{
"header": {
"name": "x-zim",
"stringMatch": {
"safeRegex": {
"regex": "gi[rR]"
}
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-foo",
"presentMatch": true
}
},
{
"header": {
"invertMatch": true,
"name": "z-bar",
"stringMatch": {
"exact": "xyz"
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-dib",
"stringMatch": {
"prefix": "gaz"
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-gir",
"stringMatch": {
"suffix": "zim"
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-zim",
"stringMatch": {
"safeRegex": {
"regex": "gi[rR]"
}
}
}
}
]
}
},
{
"notRule": {
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
}
},
{
"notRule": {
"header": {
"name": "x-baz",
"presentMatch": true
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
]
}
}
}
}
}

8
agent/xds/testdata/rbac/v2-single-permission-with-kitchen-sink-perms.golden vendored

@ -0,0 +1,8 @@
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
}

238
agent/xdsv2/rbac_resources.go

@ -226,139 +226,179 @@ func makeL7RBACPolicy(p *pbproxystate.Permission) *envoy_rbac_v3.Policy {
}
}
func permissionsFromDestinationRules(drs []*pbproxystate.DestinationRule) []*envoy_rbac_v3.Permission {
func translateRule(dr *pbproxystate.DestinationRule) *envoy_rbac_v3.Permission {
var perms []*envoy_rbac_v3.Permission
for _, dr := range drs {
// paths
switch {
case dr.PathExact != "":
perms = append(perms, &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_Exact{
Exact: dr.PathExact,
},
// paths
switch {
case dr.PathExact != "":
perms = append(perms, &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_Exact{
Exact: dr.PathExact,
},
},
},
},
})
case dr.PathPrefix != "":
perms = append(perms, &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: dr.PathPrefix,
},
},
})
case dr.PathPrefix != "":
perms = append(perms, &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: dr.PathPrefix,
},
},
},
},
})
case dr.PathRegex != "":
perms = append(perms, &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_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(dr.PathRegex),
},
},
})
case dr.PathRegex != "":
perms = append(perms, &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_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(dr.PathRegex),
},
},
},
},
})
}
},
})
}
// methods
if len(dr.Methods) > 0 {
methodHeaderRegex := strings.Join(dr.Methods, "|")
eh := &envoy_route_v3.HeaderMatcher{
Name: ":method",
HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: response.MakeEnvoyStringMatcher(methodHeaderRegex),
// methods
if len(dr.Methods) > 0 {
methodHeaderRegex := strings.Join(dr.Methods, "|")
eh := &envoy_route_v3.HeaderMatcher{
Name: ":method",
HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(methodHeaderRegex),
},
},
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
}})
},
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
}})
}
// headers
for _, hdr := range dr.DestinationRuleHeader {
eh := &envoy_route_v3.HeaderMatcher{
Name: hdr.Name,
}
// headers
for _, hdr := range dr.DestinationRuleHeader {
eh := &envoy_route_v3.HeaderMatcher{
Name: hdr.Name,
}
switch {
case hdr.Exact != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: hdr.Exact,
},
IgnoreCase: false,
switch {
case hdr.Exact != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: hdr.Exact,
},
}
case hdr.Regex != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(hdr.Regex),
},
IgnoreCase: false,
IgnoreCase: false,
},
}
case hdr.Regex != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(hdr.Regex),
},
}
IgnoreCase: false,
},
}
case hdr.Prefix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: hdr.Prefix,
},
IgnoreCase: false,
case hdr.Prefix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: hdr.Prefix,
},
}
IgnoreCase: false,
},
}
case hdr.Suffix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Suffix{
Suffix: hdr.Suffix,
},
IgnoreCase: false,
case hdr.Suffix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Suffix{
Suffix: hdr.Suffix,
},
}
case hdr.Present:
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_PresentMatch{
PresentMatch: true,
}
default:
continue // skip this impossible situation
IgnoreCase: false,
},
}
if hdr.Invert {
eh.InvertMatch = true
case hdr.Present:
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_PresentMatch{
PresentMatch: true,
}
default:
continue // skip this impossible situation
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
},
if hdr.Invert {
eh.InvertMatch = true
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
},
})
}
return combineAndPermissions(perms)
}
func permissionsFromDestinationRules(drs []*pbproxystate.DestinationRule) []*envoy_rbac_v3.Permission {
var perms []*envoy_rbac_v3.Permission
for _, dr := range drs {
subPerms := make([]*envoy_rbac_v3.Permission, len(dr.Exclude))
for i, er := range dr.Exclude {
translated := translateRule(&pbproxystate.DestinationRule{
PathExact: er.PathExact,
PathPrefix: er.PathPrefix,
PathRegex: er.PathRegex,
Methods: er.Methods,
DestinationRuleHeader: er.Headers,
})
subPerms[i] = &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_NotRule{NotRule: translated},
}
}
subPerms = append([]*envoy_rbac_v3.Permission{translateRule(dr)}, subPerms...)
perms = append(perms, combineAndPermissions(subPerms))
}
return perms
}
func combineAndPermissions(perms []*envoy_rbac_v3.Permission) *envoy_rbac_v3.Permission {
switch len(perms) {
case 0:
return anyPermission()
case 1:
return perms[0]
default:
return &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_AndRules{
AndRules: &envoy_rbac_v3.Permission_Set{
Rules: perms,
},
},
}
}
}
func toEnvoyPrincipal(p *pbproxystate.Principal) *envoy_rbac_v3.Principal {
includePrincipal := principal(p.Spiffe)

14
internal/auth/internal/types/errors.go

@ -6,10 +6,12 @@ package types
import "errors"
var (
errSourcesTenancy = errors.New("permissions sources may not specify partitions, peers, and sameness_groups together")
errSourceWildcards = errors.New("permission sources may not have wildcard namespaces and explicit names.")
errSourceExcludes = errors.New("must be defined on wildcard sources")
errInvalidPrefixValues = errors.New("prefix values, regex values, and explicit names must not combined")
errHeaderRulesInvalid = errors.New("header rule must contain header name")
ErrWildcardNotSupported = errors.New("traffic permissions without explicit destinations are not yet supported")
errSourcesTenancy = errors.New("permissions sources may not specify partitions, peers, and sameness_groups together")
errSourceWildcards = errors.New("permission sources may not have wildcard namespaces and explicit names")
errSourceExcludes = errors.New("must be defined on wildcard sources")
errInvalidPrefixValues = errors.New("prefix values, regex values, and explicit names must not combined")
errInvalidRule = errors.New("rules must contain path, method, header, or port fields")
errExclValuesMustBeSubset = errors.New("exclude permission rules must select a subset of ports and methods defined in the destination rule")
errHeaderRulesInvalid = errors.New("header rule must contain header name")
ErrWildcardNotSupported = errors.New("traffic permissions without explicit destinations are not yet supported")
)

45
internal/auth/internal/types/traffic_permissions.go

@ -4,6 +4,8 @@
package types
import (
"golang.org/x/exp/slices"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/consul/acl"
@ -254,6 +256,12 @@ func validatePermission(p *pbauth.Permission, id *pbresource.ID, wrapErr func(er
}
}
}
if dest.IsEmpty() {
merr = multierror.Append(merr, wrapDestRuleErr(resource.ErrInvalidListElement{
Name: "destination_rule",
Wrapped: errInvalidRule,
}))
}
if len(dest.Exclude) > 0 {
for e, excl := range dest.Exclude {
wrapExclPermRuleErr := func(err error) error {
@ -271,6 +279,43 @@ func validatePermission(p *pbauth.Permission, id *pbresource.ID, wrapErr func(er
Wrapped: errInvalidPrefixValues,
}))
}
for eh, hdr := range excl.Headers {
wrapExclHeaderErr := func(err error) error {
return wrapDestRuleErr(resource.ErrInvalidListElement{
Name: "exclude_permission_header_rules",
Index: eh,
Wrapped: err,
})
}
if len(hdr.Name) == 0 {
merr = multierror.Append(merr, wrapExclHeaderErr(resource.ErrInvalidListElement{
Name: "exclude_permission_header_rule",
Wrapped: errHeaderRulesInvalid,
}))
}
}
for _, m := range excl.Methods {
if len(dest.Methods) != 0 && !slices.Contains(dest.Methods, m) {
merr = multierror.Append(merr, wrapExclPermRuleErr(resource.ErrInvalidListElement{
Name: "exclude_permission_header_rule",
Wrapped: errExclValuesMustBeSubset,
}))
}
}
for _, port := range excl.PortNames {
if len(dest.PortNames) != 0 && !slices.Contains(dest.PortNames, port) {
merr = multierror.Append(merr, wrapExclPermRuleErr(resource.ErrInvalidListElement{
Name: "exclude_permission_header_rule",
Wrapped: errExclValuesMustBeSubset,
}))
}
}
if excl.IsEmpty() {
merr = multierror.Append(merr, wrapExclPermRuleErr(resource.ErrInvalidListElement{
Name: "exclude_permission_rule",
Wrapped: errInvalidRule,
}))
}
}
}
}

3
internal/auth/internal/types/traffic_permissions_ce_test.go

@ -8,10 +8,11 @@ package types
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/internal/resource/resourcetest"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/require"
)
func TestValidateTrafficPermissionsActionCE(t *testing.T) {

107
internal/auth/internal/types/traffic_permissions_test.go

@ -42,6 +42,37 @@ func TestValidateTrafficPermissions(t *testing.T) {
Action: pbauth.Action_ACTION_ALLOW,
},
},
"ok-permissions": {
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{IdentityName: "wi-1"},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "wi-2",
Namespace: "default",
Partition: "default",
},
{
IdentityName: "wi-1",
Namespace: "default",
Partition: "ap1",
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"GET"},
Headers: []*pbauth.DestinationRuleHeader{{Name: "X-Consul-Token", Present: false, Invert: true}},
PortNames: []string{"https"},
Exclude: []*pbauth.ExcludePermissionRule{{PathExact: "/admin"}},
},
},
},
},
},
},
"unspecified-action": {
// Any type other than the TrafficPermissions type would work
// to cause the error we are expecting
@ -441,6 +472,82 @@ func permissionsTestCases() map[string]permissionTestCase {
},
expectErr: `invalid element at index 0 of list "destination_rule": prefix values, regex values, and explicit names must not combined`,
},
"destination-rule-empty": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "destination_rule": rules must contain path, method, header, or port fields`,
},
"destination-rule-only-empty-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{Exclude: []*pbauth.ExcludePermissionRule{{}}},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "exclude_permission_rules": invalid element at index 0 of list "exclude_permission_rule": rules must contain path, method, header, or port fields`,
},
"destination-rule-empty-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{
PathExact: "/",
Exclude: []*pbauth.ExcludePermissionRule{{}},
},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "exclude_permission_rules": invalid element at index 0 of list "exclude_permission_rule": rules must contain path, method, header, or port fields`,
},
"destination-rule-mismatched-ports-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{
PortNames: []string{"foo"},
Exclude: []*pbauth.ExcludePermissionRule{{PortNames: []string{"bar"}}},
},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "exclude_permission_rules": invalid element at index 0 of list "exclude_permission_header_rule": exclude permission rules must select a subset of ports and methods defined in the destination rule`,
},
"destination-rule-ports-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{
Exclude: []*pbauth.ExcludePermissionRule{{PortNames: []string{"bar"}}},
},
},
},
},
"destination-rule-invalid-headers-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{
Headers: []*pbauth.DestinationRuleHeader{{Name: "auth"}},
Exclude: []*pbauth.ExcludePermissionRule{{Headers: []*pbauth.DestinationRuleHeader{{}}}},
},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "exclude_permission_header_rules": invalid element at index 0 of list "exclude_permission_header_rule": header rule must contain header name`,
},
"destination-rule-mismatched-methods-exclude": {
p: &pbauth.Permission{
Sources: []*pbauth.Source{{IdentityName: "i1"}},
DestinationRules: []*pbauth.DestinationRule{
{
Methods: []string{"post"},
Exclude: []*pbauth.ExcludePermissionRule{{Methods: []string{"patch"}}},
},
},
},
expectErr: `invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "exclude_permission_rules": invalid element at index 0 of list "exclude_permission_header_rule": exclude permission rules must select a subset of ports and methods defined in the destination rule`,
},
}
}

95
internal/mesh/internal/controllers/sidecarproxy/builder/local_app.go

@ -135,60 +135,85 @@ func destinationRulesByPort(allPorts []string, destinationRules []*pbauth.Destin
for _, p := range allPorts {
out[p] = nil
}
return out
}
for _, destinationRule := range destinationRules {
ports, dr := convertDestinationRule(allPorts, destinationRule)
for _, p := range ports {
if dr == nil {
portRules := convertDestinationRule(allPorts, destinationRule)
for p, pr := range portRules {
if pr.rule == nil {
out[p] = nil
continue
}
out[p] = append(out[p], dr)
out[p] = append(out[p], pr.rule)
}
}
return out
}
//nolint:unparam
func convertDestinationRule(allPorts []string, dr *pbauth.DestinationRule) ([]string, *pbproxystate.DestinationRule) {
ports := make(map[string]struct{})
if len(dr.PortNames) > 0 {
for _, p := range dr.PortNames {
ports[p] = struct{}{}
}
} else {
for _, p := range allPorts {
ports[p] = struct{}{}
}
}
type PortRule struct {
rule *pbproxystate.DestinationRule
}
for _, exclude := range dr.Exclude {
for _, p := range exclude.PortNames {
delete(ports, p)
func convertDestinationRule(allPorts []string, dr *pbauth.DestinationRule) map[string]*PortRule {
portRules := make(map[string]*PortRule)
targetPorts := allPorts
if len(dr.PortNames) > 0 {
targetPorts = dr.PortNames
}
for _, p := range targetPorts {
if dr.PortsOnly() {
portRules[p] = &PortRule{}
for _, exclude := range dr.Exclude {
for _, ep := range exclude.PortNames {
delete(portRules, ep)
}
}
} else {
portRules[p] = makePortRule(dr, p)
}
}
return portRules
}
var out []string
for p := range ports {
out = append(out, p)
}
if len(dr.String()) == 0 {
return out, nil
}
func makePortRule(dr *pbauth.DestinationRule, p string) *PortRule {
psdr := &pbproxystate.DestinationRule{
PathExact: dr.PathExact,
PathPrefix: dr.PathPrefix,
PathRegex: dr.PathRegex,
Methods: dr.Methods,
}
hrs := make([]*pbproxystate.DestinationRuleHeader, len(dr.Headers))
for i, hr := range dr.Headers {
psdr.DestinationRuleHeader = destinationRuleHeaders(dr.Headers)
var excls []*pbproxystate.ExcludePermissionRule
for _, ex := range dr.Exclude {
if len(ex.PortNames) == 0 || listContains(ex.PortNames, p) {
excls = append(excls, &pbproxystate.ExcludePermissionRule{
PathExact: ex.PathExact,
PathPrefix: ex.PathPrefix,
PathRegex: ex.PathRegex,
Methods: ex.Methods,
Headers: destinationRuleHeaders(ex.Headers),
})
}
}
psdr.Exclude = excls
return &PortRule{psdr}
}
func listContains(list []string, str string) bool {
for _, item := range list {
if item == str {
return true
}
}
return false
}
func destinationRuleHeaders(headers []*pbauth.DestinationRuleHeader) []*pbproxystate.DestinationRuleHeader {
hrs := make([]*pbproxystate.DestinationRuleHeader, len(headers))
for i, hr := range headers {
hrs[i] = &pbproxystate.DestinationRuleHeader{
Name: hr.Name,
Present: hr.Present,
@ -199,13 +224,7 @@ func convertDestinationRule(allPorts []string, dr *pbauth.DestinationRule) ([]st
Invert: hr.Invert,
}
}
psdr.DestinationRuleHeader = hrs
if len(psdr.String()) > 0 {
return out, psdr
}
return out, nil
return hrs
}
func makePrincipals(trustDomain string, perm *pbauth.Permission) []*pbproxystate.Principal {

386
internal/mesh/internal/controllers/sidecarproxy/builder/local_app_test.go

@ -704,6 +704,392 @@ func TestBuildTrafficPermissions(t *testing.T) {
},
},
},
"destination rules l7, exclude rules for one port": {
defaultAllow: true,
workloadPorts: map[string]*pbcatalog.WorkloadPort{
"p1": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
"p2": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
},
ctp: &pbauth.ComputedTrafficPermissions{
AllowPermissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "foo",
Partition: tenancy.Partition,
Namespace: tenancy.Namespace,
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
Exclude: []*pbauth.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbauth.DestinationRuleHeader{{Name: "restricted"}},
PortNames: []string{"p2"},
},
},
},
},
},
},
},
expected: map[string]*pbproxystate.TrafficPermissions{
"p1": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
},
},
},
},
},
"p2": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbproxystate.DestinationRuleHeader{{Name: "restricted"}},
},
},
},
},
},
},
},
},
},
"destination rules l7, rules by port": {
defaultAllow: true,
workloadPorts: map[string]*pbcatalog.WorkloadPort{
"p1": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
"p2": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
"p3": {
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
},
},
ctp: &pbauth.ComputedTrafficPermissions{
AllowPermissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "foo",
Partition: tenancy.Partition,
Namespace: tenancy.Namespace,
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
PortNames: []string{"p1", "p2"},
Exclude: []*pbauth.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbauth.DestinationRuleHeader{{Name: "restricted"}},
PortNames: []string{"p2"},
},
},
},
{
PortNames: []string{"p3"},
},
},
},
},
},
expected: map[string]*pbproxystate.TrafficPermissions{
"p1": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
},
},
},
},
},
"p2": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Methods: []string{"get", "post"},
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbproxystate.DestinationRuleHeader{{Name: "restricted"}},
},
},
},
},
},
},
},
"p3": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
},
},
},
},
},
"destination rules l7, exclude infer ports": {
defaultAllow: true,
workloadPorts: map[string]*pbcatalog.WorkloadPort{
"p1": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
"p2": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
},
ctp: &pbauth.ComputedTrafficPermissions{
AllowPermissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "foo",
Partition: tenancy.Partition,
Namespace: tenancy.Namespace,
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbauth.ExcludePermissionRule{
{
PathExact: "/secret",
},
},
},
},
},
},
},
expected: map[string]*pbproxystate.TrafficPermissions{
"p1": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
},
},
},
},
},
},
},
"p2": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
},
},
},
},
},
},
},
},
},
"destination rules l7, headers and methods exclude": {
defaultAllow: true,
workloadPorts: map[string]*pbcatalog.WorkloadPort{
"p1": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
"p2": {
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
},
ctp: &pbauth.ComputedTrafficPermissions{
AllowPermissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "foo",
Partition: tenancy.Partition,
Namespace: tenancy.Namespace,
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PathPrefix: "/",
Headers: []*pbauth.DestinationRuleHeader{
{Name: "header1", Present: true},
},
Methods: []string{"POST, GET"},
Exclude: []*pbauth.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbauth.DestinationRuleHeader{
{Name: "header2", Present: true},
},
Methods: []string{"POST"},
},
{
PathExact: "/config",
Headers: []*pbauth.DestinationRuleHeader{
{Name: "header3", Present: true},
},
Methods: []string{"GET"},
},
},
},
},
},
},
},
expected: map[string]*pbproxystate.TrafficPermissions{
"p1": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "header1", Present: true},
},
Methods: []string{"POST, GET"},
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "header2", Present: true},
},
Methods: []string{"POST"},
},
{
PathExact: "/config",
Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "header3", Present: true},
},
Methods: []string{"GET"},
},
},
},
},
},
},
},
"p2": {
DefaultAllow: false,
AllowPermissions: []*pbproxystate.Permission{
{
Principals: []*pbproxystate.Principal{
{
Spiffe: &pbproxystate.Spiffe{Regex: fmt.Sprintf("^spiffe://test.consul/ap/%s/ns/%s/identity/foo$", tenancy.Partition, tenancy.Namespace)},
},
},
DestinationRules: []*pbproxystate.DestinationRule{
{
PathPrefix: "/",
DestinationRuleHeader: []*pbproxystate.DestinationRuleHeader{
{Name: "header1", Present: true},
},
Methods: []string{"POST, GET"},
Exclude: []*pbproxystate.ExcludePermissionRule{
{
PathExact: "/secret",
Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "header2", Present: true},
},
Methods: []string{"POST"},
},
{
PathExact: "/config",
Headers: []*pbproxystate.DestinationRuleHeader{
{Name: "header3", Present: true},
},
Methods: []string{"GET"},
},
},
},
},
},
},
},
},
},
}
for name, tc := range cases {

63
proto-public/pbauth/v2beta1/traffic_permission_extras_test.go

@ -0,0 +1,63 @@
package authv2beta1
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTrafficPermissions_PortsOnly(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var dr *DestinationRule
require.False(t, dr.PortsOnly())
})
t.Run("empty", func(t *testing.T) {
dr := &DestinationRule{}
require.False(t, dr.PortsOnly())
})
t.Run("ports", func(t *testing.T) {
dr := &DestinationRule{
PortNames: []string{"foo"},
}
require.True(t, dr.PortsOnly())
})
t.Run("excl-ports", func(t *testing.T) {
dr := &DestinationRule{
Exclude: []*ExcludePermissionRule{{PortNames: []string{"foo"}}},
}
require.True(t, dr.PortsOnly())
})
t.Run("ports-and-excl-ports", func(t *testing.T) {
dr := &DestinationRule{
PortNames: []string{"foo", "bar"},
Exclude: []*ExcludePermissionRule{{PortNames: []string{"foo"}}},
}
require.True(t, dr.PortsOnly())
})
t.Run("methods", func(t *testing.T) {
dr := &DestinationRule{
Methods: []string{"put"},
}
require.False(t, dr.PortsOnly())
})
t.Run("path", func(t *testing.T) {
dr := &DestinationRule{
PathRegex: "*",
PortNames: []string{"foo"},
}
require.False(t, dr.PortsOnly())
})
t.Run("headers", func(t *testing.T) {
dr := &DestinationRule{
Exclude: []*ExcludePermissionRule{{Headers: []*DestinationRuleHeader{{Name: "Authorization"}}, PortNames: []string{"foo"}}},
}
require.False(t, dr.PortsOnly())
})
t.Run("path-and-exclports", func(t *testing.T) {
dr := &DestinationRule{
PathExact: "/",
Exclude: []*ExcludePermissionRule{{PortNames: []string{"foo"}}},
}
require.False(t, dr.PortsOnly())
})
}

60
proto-public/pbauth/v2beta1/traffic_permissions_extras.go

@ -0,0 +1,60 @@
package authv2beta1
// IsEmpty returns true if a destination rule has no fields defined.
func (d *DestinationRule) IsEmpty() bool {
if d == nil {
return true
}
return len(d.PathExact) == 0 &&
len(d.PathPrefix) == 0 &&
len(d.PathRegex) == 0 &&
len(d.Methods) == 0 &&
len(d.Headers) == 0 &&
len(d.PortNames) == 0 &&
len(d.Exclude) == 0
}
// IsEmpty returns true if an exclude permission has no fields defined.
func (e *ExcludePermissionRule) IsEmpty() bool {
if e == nil {
return true
}
return len(e.PathExact) == 0 &&
len(e.PathPrefix) == 0 &&
len(e.PathRegex) == 0 &&
len(e.Methods) == 0 &&
len(e.Headers) == 0 &&
len(e.PortNames) == 0
}
// PortsOnly returns true if a destination rule only specifies port criteria
func (d *DestinationRule) PortsOnly() bool {
if d.IsEmpty() {
return false
}
excludePortsOnly := true
for _, e := range d.Exclude {
if !e.PortsOnly() {
excludePortsOnly = false
}
}
return len(d.PathExact) == 0 &&
len(d.PathPrefix) == 0 &&
len(d.PathRegex) == 0 &&
len(d.Methods) == 0 &&
len(d.Headers) == 0 &&
(len(d.PortNames) != 0 || excludePortsOnly)
}
// PortsOnly returns true if an exclude rule only specifies port criteria
func (e *ExcludePermissionRule) PortsOnly() bool {
if e == nil {
return false
}
return len(e.PathExact) == 0 &&
len(e.PathPrefix) == 0 &&
len(e.PathRegex) == 0 &&
len(e.Methods) == 0 &&
len(e.Headers) == 0 &&
len(e.PortNames) != 0
}

10
proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.pb.binary.go

@ -66,3 +66,13 @@ func (msg *DestinationRuleHeader) MarshalBinary() ([]byte, error) {
func (msg *DestinationRuleHeader) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ExcludePermissionRule) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *ExcludePermissionRule) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

205
proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.pb.go

@ -269,6 +269,7 @@ type DestinationRule struct {
PathRegex string `protobuf:"bytes,3,opt,name=path_regex,json=pathRegex,proto3" json:"path_regex,omitempty"`
Methods []string `protobuf:"bytes,4,rep,name=methods,proto3" json:"methods,omitempty"`
DestinationRuleHeader []*DestinationRuleHeader `protobuf:"bytes,5,rep,name=destination_rule_header,json=destinationRuleHeader,proto3" json:"destination_rule_header,omitempty"`
Exclude []*ExcludePermissionRule `protobuf:"bytes,6,rep,name=exclude,proto3" json:"exclude,omitempty"`
}
func (x *DestinationRule) Reset() {
@ -338,6 +339,13 @@ func (x *DestinationRule) GetDestinationRuleHeader() []*DestinationRuleHeader {
return nil
}
func (x *DestinationRule) GetExclude() []*ExcludePermissionRule {
if x != nil {
return x.Exclude
}
return nil
}
type DestinationRuleHeader struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -433,6 +441,85 @@ func (x *DestinationRuleHeader) GetInvert() bool {
return false
}
type ExcludePermissionRule struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PathExact string `protobuf:"bytes,1,opt,name=path_exact,json=pathExact,proto3" json:"path_exact,omitempty"`
PathPrefix string `protobuf:"bytes,2,opt,name=path_prefix,json=pathPrefix,proto3" json:"path_prefix,omitempty"`
PathRegex string `protobuf:"bytes,3,opt,name=path_regex,json=pathRegex,proto3" json:"path_regex,omitempty"`
Methods []string `protobuf:"bytes,4,rep,name=methods,proto3" json:"methods,omitempty"`
Headers []*DestinationRuleHeader `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty"`
}
func (x *ExcludePermissionRule) Reset() {
*x = ExcludePermissionRule{}
if protoimpl.UnsafeEnabled {
mi := &file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExcludePermissionRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExcludePermissionRule) ProtoMessage() {}
func (x *ExcludePermissionRule) ProtoReflect() protoreflect.Message {
mi := &file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExcludePermissionRule.ProtoReflect.Descriptor instead.
func (*ExcludePermissionRule) Descriptor() ([]byte, []int) {
return file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDescGZIP(), []int{6}
}
func (x *ExcludePermissionRule) GetPathExact() string {
if x != nil {
return x.PathExact
}
return ""
}
func (x *ExcludePermissionRule) GetPathPrefix() string {
if x != nil {
return x.PathPrefix
}
return ""
}
func (x *ExcludePermissionRule) GetPathRegex() string {
if x != nil {
return x.PathRegex
}
return ""
}
func (x *ExcludePermissionRule) GetMethods() []string {
if x != nil {
return x.Methods
}
return nil
}
func (x *ExcludePermissionRule) GetHeaders() []*DestinationRuleHeader {
if x != nil {
return x.Headers
}
return nil
}
var File_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto protoreflect.FileDescriptor
var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDesc = []byte{
@ -486,7 +573,7 @@ var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDesc = []byte{
0x0a, 0x06, 0x53, 0x70, 0x69, 0x66, 0x66, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x65,
0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x67, 0x65, 0x78, 0x12, 0x1d,
0x0a, 0x0a, 0x78, 0x66, 0x63, 0x63, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x78, 0x66, 0x63, 0x63, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0x85, 0x02,
0x28, 0x09, 0x52, 0x09, 0x78, 0x66, 0x63, 0x63, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0xe2, 0x02,
0x0a, 0x0f, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c,
0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x65, 0x78, 0x61, 0x63, 0x74, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74,
@ -503,41 +590,62 @@ var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDesc = []byte{
0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x15,
0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x48,
0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0xb9, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x02,
0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a,
0x05, 0x65, 0x78, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x78,
0x61, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73,
0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66,
0x66, 0x69, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x67, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x76,
0x65, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x6e, 0x76, 0x65, 0x72,
0x74, 0x42, 0xdd, 0x02, 0x0a, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63,
0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e,
0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73,
0x74, 0x61, 0x74, 0x65, 0x42, 0x17, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x50, 0x65, 0x72,
0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a,
0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68,
0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79,
0x73, 0x74, 0x61, 0x74, 0x65, 0xa2, 0x02, 0x05, 0x48, 0x43, 0x4d, 0x56, 0x50, 0xaa, 0x02, 0x2a,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x2e, 0x4d, 0x65, 0x73, 0x68, 0x2e, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x50, 0x62,
0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0xca, 0x02, 0x2a, 0x48, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65,
0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, 0x50, 0x62, 0x70, 0x72, 0x6f,
0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0xe2, 0x02, 0x36, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63,
0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x5b, 0x0a, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65,
0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76,
0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74,
0x61, 0x74, 0x65, 0x2e, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75,
0x64, 0x65, 0x22, 0xb9, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x08, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78,
0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x78, 0x61, 0x63, 0x74,
0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66,
0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78,
0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x72, 0x65, 0x67, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x74,
0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x22, 0xed,
0x01, 0x0a, 0x15, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x74, 0x68,
0x5f, 0x65, 0x78, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61,
0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x74, 0x68, 0x5f,
0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61,
0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x74, 0x68,
0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61,
0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f,
0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
0x73, 0x12, 0x5b, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63,
0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74,
0x61, 0x31, 0x2e, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e,
0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x48,
0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x42, 0xdd,
0x02, 0x0a, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62,
0x65, 0x74, 0x61, 0x31, 0x2e, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74,
0x65, 0x42, 0x17, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x44, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x32,
0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x70, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61,
0x74, 0x65, 0xa2, 0x02, 0x05, 0x48, 0x43, 0x4d, 0x56, 0x50, 0xaa, 0x02, 0x2a, 0x48, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x4d, 0x65,
0x73, 0x68, 0x2e, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x50, 0x62, 0x70, 0x72, 0x6f,
0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0xca, 0x02, 0x2a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63,
0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c,
0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, 0x50, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73,
0x74, 0x61, 0x74, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0xea, 0x02, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x68, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x65,
0x74, 0x61, 0x31, 0x3a, 0x3a, 0x50, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74,
0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x74, 0x61, 0x74, 0x65, 0xe2, 0x02, 0x36, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62,
0x65, 0x74, 0x61, 0x31, 0x5c, 0x50, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74,
0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x2e,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75,
0x6c, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x68, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31,
0x3a, 0x3a, 0x50, 0x62, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x73, 0x74, 0x61, 0x74, 0x65, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -552,7 +660,7 @@ func file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDescGZIP() []
return file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDescData
}
var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_goTypes = []interface{}{
(*TrafficPermissions)(nil), // 0: hashicorp.consul.mesh.v2beta1.pbproxystate.TrafficPermissions
(*Permission)(nil), // 1: hashicorp.consul.mesh.v2beta1.pbproxystate.Permission
@ -560,6 +668,7 @@ var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_goTypes = []inter
(*Spiffe)(nil), // 3: hashicorp.consul.mesh.v2beta1.pbproxystate.Spiffe
(*DestinationRule)(nil), // 4: hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRule
(*DestinationRuleHeader)(nil), // 5: hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRuleHeader
(*ExcludePermissionRule)(nil), // 6: hashicorp.consul.mesh.v2beta1.pbproxystate.ExcludePermissionRule
}
var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_depIdxs = []int32{
1, // 0: hashicorp.consul.mesh.v2beta1.pbproxystate.TrafficPermissions.allow_permissions:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.Permission
@ -569,11 +678,13 @@ var file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_depIdxs = []int32
3, // 4: hashicorp.consul.mesh.v2beta1.pbproxystate.Principal.spiffe:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.Spiffe
3, // 5: hashicorp.consul.mesh.v2beta1.pbproxystate.Principal.exclude_spiffes:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.Spiffe
5, // 6: hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRule.destination_rule_header:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRuleHeader
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
6, // 7: hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRule.exclude:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.ExcludePermissionRule
5, // 8: hashicorp.consul.mesh.v2beta1.pbproxystate.ExcludePermissionRule.headers:type_name -> hashicorp.consul.mesh.v2beta1.pbproxystate.DestinationRuleHeader
9, // [9:9] is the sub-list for method output_type
9, // [9:9] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_init() }
@ -654,6 +765,18 @@ func file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_init() {
return nil
}
}
file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExcludePermissionRule); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@ -661,7 +784,7 @@ func file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pbmesh_v2beta1_pbproxystate_traffic_permissions_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},

9
proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions.proto

@ -42,6 +42,7 @@ message DestinationRule {
string path_regex = 3;
repeated string methods = 4;
repeated DestinationRuleHeader destination_rule_header = 5;
repeated ExcludePermissionRule exclude = 6;
}
message DestinationRuleHeader {
@ -53,3 +54,11 @@ message DestinationRuleHeader {
string regex = 6;
bool invert = 7;
}
message ExcludePermissionRule {
string path_exact = 1;
string path_prefix = 2;
string path_regex = 3;
repeated string methods = 4;
repeated DestinationRuleHeader headers = 5;
}

21
proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions_deepcopy.gen.go

@ -130,3 +130,24 @@ func (in *DestinationRuleHeader) DeepCopy() *DestinationRuleHeader {
func (in *DestinationRuleHeader) DeepCopyInterface() interface{} {
return in.DeepCopy()
}
// DeepCopyInto supports using ExcludePermissionRule within kubernetes types, where deepcopy-gen is used.
func (in *ExcludePermissionRule) DeepCopyInto(out *ExcludePermissionRule) {
proto.Reset(out)
proto.Merge(out, proto.Clone(in))
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExcludePermissionRule. Required by controller-gen.
func (in *ExcludePermissionRule) DeepCopy() *ExcludePermissionRule {
if in == nil {
return nil
}
out := new(ExcludePermissionRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new ExcludePermissionRule. Required by controller-gen.
func (in *ExcludePermissionRule) DeepCopyInterface() interface{} {
return in.DeepCopy()
}

11
proto-public/pbmesh/v2beta1/pbproxystate/traffic_permissions_json.gen.go

@ -71,6 +71,17 @@ func (this *DestinationRuleHeader) UnmarshalJSON(b []byte) error {
return TrafficPermissionsUnmarshaler.Unmarshal(b, this)
}
// MarshalJSON is a custom marshaler for ExcludePermissionRule
func (this *ExcludePermissionRule) MarshalJSON() ([]byte, error) {
str, err := TrafficPermissionsMarshaler.Marshal(this)
return []byte(str), err
}
// UnmarshalJSON is a custom unmarshaler for ExcludePermissionRule
func (this *ExcludePermissionRule) UnmarshalJSON(b []byte) error {
return TrafficPermissionsUnmarshaler.Unmarshal(b, this)
}
var (
TrafficPermissionsMarshaler = &protojson.MarshalOptions{}
TrafficPermissionsUnmarshaler = &protojson.UnmarshalOptions{DiscardUnknown: false}

459
test-integ/catalogv2/traffic_permissions_test.go

@ -0,0 +1,459 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package catalogv2
import (
"context"
"fmt"
"net/http"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/test-integ/topoutil"
libassert "github.com/hashicorp/consul/test/integration/consul-container/libs/assert"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"github.com/hashicorp/consul/testing/deployer/sprawl/sprawltest"
"github.com/hashicorp/consul/testing/deployer/topology"
)
type testCase struct {
permissions []*permission
result []*testResult
}
type permission struct {
allow bool
excludeSource bool
includeSourceTenancy bool
excludeSourceTenancy bool
destRules []*destRules
}
type destRules struct {
values *ruleValues
excludes []*ruleValues
}
type ruleValues struct {
portNames []string
path string
pathPref string
pathReg string
headers []string
methods []string
}
type testResult struct {
fail bool
port string
path string
headers map[string]string
}
func newTrafficPermissions(p *permission, srcTenancy *pbresource.Tenancy) *pbauth.TrafficPermissions {
sources := []*pbauth.Source{{
IdentityName: "static-client",
Namespace: srcTenancy.Namespace,
Partition: srcTenancy.Partition,
}}
destinationRules := []*pbauth.DestinationRule{}
if p != nil {
srcId := "static-client"
if p.includeSourceTenancy {
srcId = ""
}
if p.excludeSource {
sources = []*pbauth.Source{{
IdentityName: srcId,
Namespace: srcTenancy.Namespace,
Partition: srcTenancy.Partition,
Exclude: []*pbauth.ExcludeSource{{
IdentityName: "static-client",
Namespace: srcTenancy.Namespace,
Partition: srcTenancy.Partition,
}},
}}
} else {
sources = []*pbauth.Source{{
IdentityName: srcId,
Namespace: srcTenancy.Namespace,
Partition: srcTenancy.Partition,
}}
}
for _, dr := range p.destRules {
destRule := &pbauth.DestinationRule{}
if dr.values != nil {
destRule.PathExact = dr.values.path
destRule.PathPrefix = dr.values.pathPref
destRule.PathRegex = dr.values.pathReg
destRule.Methods = dr.values.methods
destRule.PortNames = dr.values.portNames
destRule.Headers = []*pbauth.DestinationRuleHeader{}
for _, h := range dr.values.headers {
destRule.Headers = append(destRule.Headers, &pbauth.DestinationRuleHeader{
Name: h,
Present: true,
})
}
}
var excludePermissions []*pbauth.ExcludePermissionRule
for _, e := range dr.excludes {
eRule := &pbauth.ExcludePermissionRule{
PathExact: e.path,
PathPrefix: e.pathPref,
PathRegex: e.pathReg,
Methods: e.methods,
PortNames: e.portNames,
}
eRule.Headers = []*pbauth.DestinationRuleHeader{}
for _, h := range e.headers {
eRule.Headers = append(eRule.Headers, &pbauth.DestinationRuleHeader{
Name: h,
Present: true,
})
}
excludePermissions = append(excludePermissions, eRule)
}
destRule.Exclude = excludePermissions
destinationRules = append(destinationRules, destRule)
}
}
action := pbauth.Action_ACTION_ALLOW
if !p.allow {
action = pbauth.Action_ACTION_DENY
}
return &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "static-server",
},
Action: action,
Permissions: []*pbauth.Permission{{
Sources: sources,
DestinationRules: destinationRules,
}},
}
}
// This tests runs a gauntlet of traffic permissions updates and validates that the request status codes match the intended rules
func TestL7TrafficPermissions(t *testing.T) {
testcases := map[string]testCase{
// L4 permissions
"basic": {permissions: []*permission{{allow: true}}, result: []*testResult{{fail: false}}},
"client-exclude": {permissions: []*permission{{allow: true, includeSourceTenancy: true, excludeSource: true}}, result: []*testResult{{fail: true}}},
"allow-all-client-in-tenancy": {permissions: []*permission{{allow: true, includeSourceTenancy: true}}, result: []*testResult{{fail: false}}},
"only-one-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{portNames: []string{"http"}}}}}}, result: []*testResult{{fail: true, port: "http2"}}},
"exclude-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{excludes: []*ruleValues{{portNames: []string{"http"}}}}}}}, result: []*testResult{{fail: true, port: "http"}}},
// L7 permissions
"methods": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{methods: []string{"POST", "PUT", "PATCH", "DELETE", "CONNECT", "HEAD", "OPTIONS", "TRACE"}, pathPref: "/"}}}}},
// fortio fetch2 is configured to GET
result: []*testResult{{fail: true}}},
"headers": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{headers: []string{"a", "b"}, pathPref: "/"}}}}},
result: []*testResult{{fail: true}, {fail: true, headers: map[string]string{"a": "1"}}, {fail: false, headers: map[string]string{"a": "1", "b": "2"}}}},
"path-prefix-all": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/", methods: []string{"GET"}}}}}}, result: []*testResult{{fail: false}}},
"method-exclude": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}, excludes: []*ruleValues{{methods: []string{"GET"}}}}}}},
// fortio fetch2 is configured to GET
result: []*testResult{{fail: true}}},
"exclude-paths-and-headers": {permissions: []*permission{{allow: true, destRules: []*destRules{
{
values: &ruleValues{pathPref: "/f", headers: []string{"a"}},
excludes: []*ruleValues{{headers: []string{"b"}, path: "/foobar"}},
}}}},
result: []*testResult{
{fail: false, path: "foobar", headers: map[string]string{"a": "1"}},
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}},
{fail: true, path: "foobar", headers: map[string]string{"a": "1", "b": "2"}},
{fail: false, path: "foo", headers: map[string]string{"a": "1"}},
{fail: true, path: "foo", headers: map[string]string{"b": "2"}},
{fail: true, path: "baz", headers: map[string]string{"a": "1"}},
}},
"exclude-paths-or-headers": {permissions: []*permission{{allow: true, destRules: []*destRules{
{values: &ruleValues{pathPref: "/f", headers: []string{"a"}}, excludes: []*ruleValues{{headers: []string{"b"}}, {path: "/foobar"}}}}}},
result: []*testResult{
{fail: true, path: "foobar", headers: map[string]string{"a": "1"}},
{fail: true, path: "foo", headers: map[string]string{"a": "1", "b": "2"}},
{fail: true, path: "foobar", headers: map[string]string{"a": "1", "b": "2"}},
{fail: false, path: "foo", headers: map[string]string{"a": "1"}},
{fail: false, path: "foo", headers: map[string]string{"a": "1"}},
{fail: true, path: "baz", port: "http", headers: map[string]string{"a": "1"}},
}},
"path-or-header": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar"}}, {values: &ruleValues{headers: []string{"b"}}}}}},
result: []*testResult{
{fail: false, path: "bar"},
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}},
{fail: false, path: "bar", headers: map[string]string{"b": "2"}},
{fail: true, path: "foo", headers: map[string]string{"a": "1"}},
}},
"path-and-header": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar", headers: []string{"b"}}}}}},
result: []*testResult{
{fail: true, path: "bar"},
{fail: true, path: "foo", headers: map[string]string{"a": "1", "b": "2"}},
{fail: false, path: "bar", headers: map[string]string{"b": "2"}},
{fail: true, path: "foo", headers: map[string]string{"a": "1"}},
}},
"path-regex-exclude": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}, excludes: []*ruleValues{{pathReg: ".*dns.*"}}}}}},
result: []*testResult{{fail: true, path: "fortio/rest/dns"}, {fail: false, path: "fortio/rest/status"}}},
"header-include-exclude-by-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/", headers: []string{"experiment1", "experiment2"}}, excludes: []*ruleValues{{portNames: []string{"http2"}, headers: []string{"experiment1"}}}}}}},
result: []*testResult{{fail: true, port: "http2", headers: map[string]string{"experiment1": "a", "experiment2": "b"}},
{fail: false, port: "http", headers: map[string]string{"experiment1": "a", "experiment2": "b"}},
{fail: true, port: "http2", headers: map[string]string{"experiment2": "b"}},
{fail: true, port: "http", headers: map[string]string{"experiment3": "c"}},
}},
"two-tp-or": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar"}}}}, {allow: true, destRules: []*destRules{{values: &ruleValues{headers: []string{"b"}}}}}},
result: []*testResult{
{fail: false, path: "bar"},
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}},
{fail: false, path: "bar", headers: map[string]string{"b": "2"}},
{fail: true, path: "foo", headers: map[string]string{"a": "1"}},
}},
}
if utils.IsEnterprise() {
// DENY and ALLOW permissions
testcases["deny-cancel-allow"] = testCase{permissions: []*permission{{allow: true}, {allow: false}}, result: []*testResult{{fail: true}}}
testcases["l4-deny-l7-allow"] = testCase{permissions: []*permission{{allow: false}, {allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}}}}}, result: []*testResult{{fail: true}, {fail: true, path: "test"}}}
testcases["l7-deny-l4-allow"] = testCase{permissions: []*permission{{allow: true}, {allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}}}}, {allow: false, destRules: []*destRules{{values: &ruleValues{pathPref: "/foo"}}}}},
result: []*testResult{{fail: false}, {fail: false, path: "test"}, {fail: true, path: "foo-bar"}}}
}
tenancies := []*pbresource.Tenancy{
{
Partition: "default",
Namespace: "default",
},
}
if utils.IsEnterprise() {
tenancies = append(tenancies, &pbresource.Tenancy{
Partition: "ap1",
Namespace: "ns1",
})
}
cfg := testL7TrafficPermissionsCreator{tenancies}.NewConfig(t)
targetImage := utils.TargetImages()
imageName := targetImage.Consul
if utils.IsEnterprise() {
imageName = targetImage.ConsulEnterprise
}
t.Log("running with target image: " + imageName)
sp := sprawltest.Launch(t, cfg)
asserter := topoutil.NewAsserter(sp)
topo := sp.Topology()
cluster := topo.Clusters["dc1"]
ships := topo.ComputeRelationships()
clientV2 := sp.ResourceServiceClientForCluster(cluster.Name)
// Make sure services exist
for _, tenancy := range tenancies {
for _, name := range []string{
"static-server",
"static-client",
} {
libassert.CatalogV2ServiceHasEndpointCount(t, clientV2, name, tenancy, len(tenancies))
}
}
var initialTrafficPerms []*pbresource.Resource
for testName, tc := range testcases {
// Delete old TP and write new one for a new test case
mustDeleteTestResources(t, clientV2, initialTrafficPerms)
initialTrafficPerms = []*pbresource.Resource{}
for _, st := range tenancies {
for _, dt := range tenancies {
for i, p := range tc.permissions {
newTrafficPerms := sprawltest.MustSetResourceData(t, &pbresource.Resource{
Id: &pbresource.ID{
Type: pbauth.TrafficPermissionsType,
Name: "static-server-perms" + strconv.Itoa(i) + "-" + st.Namespace + "-" + st.Partition,
Tenancy: dt,
},
}, newTrafficPermissions(p, st))
mustWriteTestResource(t, clientV2, newTrafficPerms)
initialTrafficPerms = append(initialTrafficPerms, newTrafficPerms)
}
}
}
t.Log(initialTrafficPerms)
// Wait for the resource updates to go through and Envoy to be ready
time.Sleep(1 * time.Second)
// Check the default server workload envoy config for RBAC filters matching testcase criteria
serverWorkload := cluster.WorkloadsByID(topology.ID{
Partition: "default",
Namespace: "default",
Name: "static-server",
})
asserter.AssertEnvoyHTTPrbacFiltersContainIntentions(t, serverWorkload[0])
// Check relationships
for _, ship := range ships {
t.Run("case: "+testName+":"+ship.Destination.PortName+":("+ship.Caller.ID.Partition+"/"+ship.Caller.ID.Namespace+
")("+ship.Destination.ID.Partition+"/"+ship.Destination.ID.Namespace+")", func(t *testing.T) {
var (
wrk = ship.Caller
dest = ship.Destination
)
for _, res := range tc.result {
if res.port != "" && res.port != ship.Destination.PortName {
continue
}
dest.ID.Name = "static-server"
destClusterPrefix := clusterPrefix(dest.PortName, dest.ID, dest.Cluster)
asserter.DestinationEndpointStatus(t, wrk, destClusterPrefix+".", "HEALTHY", len(tenancies))
status := http.StatusForbidden
if res.fail == false {
status = http.StatusOK
}
t.Log("Test request:"+res.path, res.headers, status)
asserter.FortioFetch2ServiceStatusCodes(t, wrk, dest, res.path, res.headers, []int{status})
}
})
}
}
}
func mustWriteTestResource(t *testing.T, client pbresource.ResourceServiceClient, res *pbresource.Resource) {
retryer := &retry.Timer{Timeout: time.Minute, Wait: time.Second}
rsp, err := client.Write(context.Background(), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
retry.RunWith(retryer, t, func(r *retry.R) {
readRsp, err := client.Read(context.Background(), &pbresource.ReadRequest{Id: rsp.Resource.Id})
require.NoError(r, err, "error reading %s", rsp.Resource.Id.Name)
require.NotNil(r, readRsp)
})
}
func mustDeleteTestResources(t *testing.T, client pbresource.ResourceServiceClient, resources []*pbresource.Resource) {
if len(resources) == 0 {
return
}
retryer := &retry.Timer{Timeout: time.Minute, Wait: time.Second}
for _, res := range resources {
retry.RunWith(retryer, t, func(r *retry.R) {
_, err := client.Delete(context.Background(), &pbresource.DeleteRequest{Id: res.Id})
if status.Code(err) == codes.NotFound {
return
}
if err != nil && status.Code(err) != codes.Aborted {
r.Stop(fmt.Errorf("failed to delete the resource: %w", err))
return
}
require.NoError(r, err)
})
}
}
type testL7TrafficPermissionsCreator struct {
tenancies []*pbresource.Tenancy
}
func (c testL7TrafficPermissionsCreator) NewConfig(t *testing.T) *topology.Config {
const clusterName = "dc1"
servers := topoutil.NewTopologyServerSet(clusterName+"-server", 1, []string{clusterName, "wan"}, nil)
cluster := &topology.Cluster{
Enterprise: utils.IsEnterprise(),
Name: clusterName,
Nodes: servers,
}
lastNode := 0
nodeName := func() string {
lastNode++
return fmt.Sprintf("%s-box%d", clusterName, lastNode)
}
for _, st := range c.tenancies {
for _, dt := range c.tenancies {
c.topologyConfigAddNodes(cluster, nodeName, st, dt)
}
}
return &topology.Config{
Images: utils.TargetImages(),
Networks: []*topology.Network{
{Name: clusterName},
{Name: "wan", Type: "wan"},
},
Clusters: []*topology.Cluster{
cluster,
},
}
}
func (c testL7TrafficPermissionsCreator) topologyConfigAddNodes(
cluster *topology.Cluster,
nodeName func() string,
sourceTenancy *pbresource.Tenancy,
destinationTenancy *pbresource.Tenancy,
) {
clusterName := cluster.Name
newID := func(name string, tenancy *pbresource.Tenancy) topology.ID {
return topology.ID{
Partition: tenancy.Partition,
Namespace: tenancy.Namespace,
Name: name,
}
}
serverNode := &topology.Node{
Kind: topology.NodeKindDataplane,
Version: topology.NodeVersionV2,
Partition: destinationTenancy.Partition,
Name: nodeName(),
Workloads: []*topology.Workload{
topoutil.NewFortioWorkloadWithDefaults(
clusterName,
newID("static-server", destinationTenancy),
topology.NodeVersionV2,
nil,
),
},
}
clientNode := &topology.Node{
Kind: topology.NodeKindDataplane,
Version: topology.NodeVersionV2,
Partition: sourceTenancy.Partition,
Name: nodeName(),
Workloads: []*topology.Workload{
topoutil.NewFortioWorkloadWithDefaults(
clusterName,
newID("static-client", sourceTenancy),
topology.NodeVersionV2,
func(wrk *topology.Workload) {
wrk.Destinations = append(wrk.Destinations, &topology.Destination{
ID: newID("static-server", destinationTenancy),
PortName: "http",
LocalAddress: "0.0.0.0", // needed for an assertion
LocalPort: 5000,
},
&topology.Destination{
ID: newID("static-server", destinationTenancy),
PortName: "http2",
LocalAddress: "0.0.0.0", // needed for an assertion
LocalPort: 5001,
},
)
wrk.WorkloadIdentity = "static-client"
},
),
},
}
cluster.Nodes = append(cluster.Nodes,
clientNode,
serverNode,
)
}

54
test-integ/topoutil/asserter.go

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"regexp"
"strings"
"testing"
"time"
@ -127,6 +128,41 @@ func (a *Asserter) AssertEnvoyPresentsCertURIWithClient(t *testing.T, workload *
libassert.AssertEnvoyPresentsCertURIWithClient(t, client, addr, workload.ID.Name)
}
func (a *Asserter) AssertEnvoyHTTPrbacFiltersContainIntentions(t *testing.T, workload *topology.Workload) {
t.Helper()
client, addr := a.getEnvoyClient(t, workload)
var (
dump string
err error
)
failer := func() *retry.Timer {
return &retry.Timer{Timeout: 30 * time.Second, Wait: 1 * time.Second}
}
retry.RunWith(failer(), t, func(r *retry.R) {
dump, _, err = libassert.GetEnvoyOutputWithClient(client, addr, "config_dump", map[string]string{})
if err != nil {
r.Fatal("could not fetch envoy configuration")
}
})
// the steps below validate that the json result from envoy config dump configured active listeners with rbac and http filters
filter := `.configs[2].dynamic_listeners[].active_state.listener | "\(.name) \( .filter_chains[].filters[] | select(.name == "envoy.filters.network.http_connection_manager") | .typed_config.http_filters | map(.name) | join(","))"`
errorStateFilter := `.configs[2].dynamic_listeners[].error_state"`
configErrorStates, _ := utils.JQFilter(dump, errorStateFilter)
require.Nil(t, configErrorStates, "there should not be any error states on listener configuration")
results, err := utils.JQFilter(dump, filter)
require.NoError(t, err, "could not parse envoy configuration")
var filteredResult []string
for _, result := range results {
parts := strings.Split(strings.ReplaceAll(result, `,`, " "), " ")
sanitizedResult := append(parts[:0], parts[1:]...)
filteredResult = append(filteredResult, sanitizedResult...)
}
require.Contains(t, filteredResult, "envoy.filters.http.rbac")
require.Contains(t, filteredResult, "envoy.filters.http.router")
require.Contains(t, dump, "intentions")
}
// HTTPServiceEchoes verifies that a post to the given ip/port combination
// returns the data in the response body. Optional path can be provided to
// differentiate requests.
@ -243,7 +279,7 @@ func (a *Asserter) fortioFetch2Destination(
) (body []byte, res *http.Response) {
t.Helper()
err, res := getFortioFetch2DestinationResponse(t, client, addr, dest, path)
err, res := getFortioFetch2DestinationResponse(t, client, addr, dest, path, nil)
require.NoError(t, err)
defer res.Body.Close()
@ -258,7 +294,7 @@ func (a *Asserter) fortioFetch2Destination(
return body, res
}
func getFortioFetch2DestinationResponse(t testutil.TestingTB, client *http.Client, addr string, dest *topology.Destination, path string) (error, *http.Response) {
func getFortioFetch2DestinationResponse(t testutil.TestingTB, client *http.Client, addr string, dest *topology.Destination, path string, headers map[string]string) (error, *http.Response) {
var actualURL string
if dest.Implied {
actualURL = fmt.Sprintf("http://%s--%s--%s.virtual.consul:%d/%s",
@ -279,6 +315,9 @@ func getFortioFetch2DestinationResponse(t testutil.TestingTB, client *http.Clien
req, err := http.NewRequest(http.MethodPost, url, nil)
require.NoError(t, err)
for h, v := range headers {
req.Header.Set(h, v)
}
res, err := client.Do(req)
require.NoError(t, err)
return err, res
@ -339,13 +378,16 @@ func (a *Asserter) FortioFetch2FortioName(
})
}
// FortioFetch2ServiceUnavailable uses the /fortio/fetch2 endpoint to do a header echo check against an destination
// fortio and asserts that the service is unavailable (503)
func (a *Asserter) FortioFetch2ServiceUnavailable(t *testing.T, fortioWrk *topology.Workload, dest *topology.Destination) {
const kPassphrase = "x-passphrase"
const passphrase = "hello"
path := (fmt.Sprintf("/?header=%s:%s", kPassphrase, passphrase))
a.FortioFetch2ServiceStatusCodes(t, fortioWrk, dest, path, nil, []int{http.StatusServiceUnavailable})
}
// FortioFetch2ServiceStatusCodes uses the /fortio/fetch2 endpoint to do a header echo check against a destination
// fortio and asserts that the returned status code matches the desired one(s)
func (a *Asserter) FortioFetch2ServiceStatusCodes(t *testing.T, fortioWrk *topology.Workload, dest *topology.Destination, path string, headers map[string]string, statuses []int) {
var (
node = fortioWrk.Node
addr = fmt.Sprintf("%s:%d", node.LocalAddress(), fortioWrk.PortOrDefault(dest.PortName))
@ -353,9 +395,9 @@ func (a *Asserter) FortioFetch2ServiceUnavailable(t *testing.T, fortioWrk *topol
)
retry.RunWith(&retry.Timer{Timeout: 60 * time.Second, Wait: time.Millisecond * 500}, t, func(r *retry.R) {
_, res := getFortioFetch2DestinationResponse(r, client, addr, dest, path)
_, res := getFortioFetch2DestinationResponse(r, client, addr, dest, path, headers)
defer res.Body.Close()
require.Equal(r, http.StatusServiceUnavailable, res.StatusCode)
require.Contains(r, statuses, res.StatusCode)
})
}

35
test/integration/consul-container/libs/assert/envoy.go

@ -198,41 +198,6 @@ func AssertEnvoyMetricAtLeast(t *testing.T, adminPort int, prefix, metric string
})
}
// GetEnvoyHTTPrbacFilters validates that proxy was configured with an http connection manager
// AssertEnvoyHTTPrbacFilters validates that proxy was configured with an http connection manager
// this assertion is currently unused current tests use http protocol
func AssertEnvoyHTTPrbacFilters(t *testing.T, port int) {
var (
dump string
err error
)
failer := func() *retry.Timer {
return &retry.Timer{Timeout: 30 * time.Second, Wait: 1 * time.Second}
}
retry.RunWith(failer(), t, func(r *retry.R) {
dump, _, err = GetEnvoyOutput(port, "config_dump", map[string]string{})
if err != nil {
r.Fatal("could not fetch envoy configuration")
}
})
// the steps below validate that the json result from envoy config dump configured active listeners with rbac and http filters
filter := `.configs[2].dynamic_listeners[].active_state.listener | "\(.name) \( .filter_chains[0].filters[] | select(.name == "envoy.filters.network.http_connection_manager") | .typed_config.http_filters | map(.name) | join(","))"`
results, err := utils.JQFilter(dump, filter)
require.NoError(t, err, "could not parse envoy configuration")
require.Len(t, results, 1, "static-server proxy should have been configured with two listener filters.")
var filteredResult []string
for _, result := range results {
sanitizedResult := sanitizeResult(result)
filteredResult = append(filteredResult, sanitizedResult...)
}
require.Contains(t, filteredResult, "envoy.filters.http.rbac")
assert.Contains(t, filteredResult, "envoy.filters.http.header_to_metadata")
assert.Contains(t, filteredResult, "envoy.filters.http.router")
}
// AssertEnvoyPresentsCertURI makes GET request to /certs endpoint and validates that
// two certificates URI is available in the response
func AssertEnvoyPresentsCertURI(t *testing.T, port int, serviceName string) {

Loading…
Cancel
Save