From 55e283dda9b7620fd1d6fb658d0b762ddea2e16e Mon Sep 17 00:00:00 2001 From: Ronald Date: Tue, 30 May 2023 13:38:33 -0400 Subject: [PATCH] [NET-3092] JWT Verify claims handling (#17452) * [NET-3092] JWT Verify claims handling --- .changelog/17452.txt | 3 + agent/xds/jwt_authn.go | 95 +++++-- agent/xds/jwt_authn_test.go | 187 ++++++++++---- agent/xds/rbac.go | 168 ++++++++++++- agent/xds/rbac_test.go | 236 ++++++++++++++++++ .../jwt_authn/intention-with-path.golden | 7 +- .../testdata/jwt_authn/local-provider.golden | 1 + ...ltiple-providers-and-one-permission.golden | 20 +- .../testdata/jwt_authn/remote-provider.golden | 1 + .../top-level-provider-with-permission.golden | 19 +- ...jwt-with-one-permission--httpfilter.golden | 59 +++++ ...y-top-level-jwt-with-one-permission.golden | 10 + ...evel-jwt-no-permissions--httpfilter.golden | 57 +++++ .../rbac/top-level-jwt-no-permissions.golden | 32 +++ ...th-multiple-permissions--httpfilter.golden | 113 +++++++++ ...level-jwt-with-multiple-permissions.golden | 10 + ...jwt-with-one-permission--httpfilter.golden | 98 ++++++++ .../top-level-jwt-with-one-permission.golden | 10 + 18 files changed, 1035 insertions(+), 91 deletions(-) create mode 100644 .changelog/17452.txt create mode 100644 agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden create mode 100644 agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-no-permissions.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden create mode 100644 agent/xds/testdata/rbac/top-level-jwt-with-one-permission.golden diff --git a/.changelog/17452.txt b/.changelog/17452.txt new file mode 100644 index 0000000000..2f40095dcd --- /dev/null +++ b/.changelog/17452.txt @@ -0,0 +1,3 @@ +```release-note:feature +mesh: Support configuring JWT authentication in Envoy. +``` diff --git a/agent/xds/jwt_authn.go b/agent/xds/jwt_authn.go index 92c8670355..0dc95f5eec 100644 --- a/agent/xds/jwt_authn.go +++ b/agent/xds/jwt_authn.go @@ -16,6 +16,18 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" ) +const ( + jwtEnvoyFilter = "envoy.filters.http.jwt_authn" + jwtMetadataKeyPrefix = "jwt_payload" +) + +// This is an intermediate JWTProvider form used to associate +// unique payload keys to providers +type jwtAuthnProvider struct { + ComputedName string + Provider *structs.IntentionJWTProvider +} + func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) { providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{} var rules []*envoy_http_jwt_authn_v3.RequirementRule @@ -24,29 +36,33 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention if intention.JWT == nil && !hasJWTconfig(intention.Permissions) { continue } - for _, jwtReq := range collectJWTRequirements(intention) { - if _, ok := providers[jwtReq.Name]; ok { + for _, jwtReq := range collectJWTAuthnProviders(intention) { + if _, ok := providers[jwtReq.ComputedName]; ok { continue } - jwtProvider, ok := pCE[jwtReq.Name] + jwtProvider, ok := pCE[jwtReq.Provider.Name] if !ok { - return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Name) + return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Provider.Name) } - envoyCfg, err := buildJWTProviderConfig(jwtProvider) + // If intention permissions use HTTP-match criteria with + // VerifyClaims, then generate a clone of the jwt provider with a + // unique key for payload_in_metadata. The RBAC filter relies on + // the key to check the correct claims for the matched request. + envoyCfg, err := buildJWTProviderConfig(jwtProvider, jwtReq.ComputedName) if err != nil { return nil, err } - providers[jwtReq.Name] = envoyCfg + providers[jwtReq.ComputedName] = envoyCfg } - for _, perm := range intention.Permissions { + for k, perm := range intention.Permissions { if perm.JWT == nil { continue } for _, prov := range perm.JWT.Providers { - rule := buildRouteRule(prov, perm, "/") + rule := buildRouteRule(prov, perm, "/", k) rules = append(rules, rule) } } @@ -54,8 +70,7 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention if intention.JWT != nil { for _, provider := range intention.JWT.Providers { // The top-level provider applies to all requests. - // TODO(roncodingenthusiast): Handle provider.VerifyClaims - rule := buildRouteRule(provider, nil, "/") + rule := buildRouteRule(provider, nil, "/", 0) rules = append(rules, rule) } } @@ -70,37 +85,65 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention Providers: providers, Rules: rules, } - return makeEnvoyHTTPFilter("envoy.filters.http.jwt_authn", cfg) + return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg) } -func collectJWTRequirements(i *structs.Intention) []*structs.IntentionJWTProvider { - var jReqs []*structs.IntentionJWTProvider +func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider { + var reqs []*jwtAuthnProvider if i.JWT != nil { - jReqs = append(jReqs, i.JWT.Providers...) + for _, prov := range i.JWT.Providers { + reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, nil, 0)}) + } } - jReqs = append(jReqs, getPermissionsProviders(i.Permissions)...) + reqs = append(reqs, getPermissionsProviders(i.Permissions)...) - return jReqs + return reqs } -func getPermissionsProviders(p []*structs.IntentionPermission) []*structs.IntentionJWTProvider { - intentionProviders := []*structs.IntentionJWTProvider{} - for _, perm := range p { +func getPermissionsProviders(p []*structs.IntentionPermission) []*jwtAuthnProvider { + var reqs []*jwtAuthnProvider + for k, perm := range p { if perm.JWT == nil { continue } - intentionProviders = append(intentionProviders, perm.JWT.Providers...) + for _, prov := range perm.JWT.Providers { + reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, perm, k)}) + } } - return intentionProviders + return reqs } -func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry) (*envoy_http_jwt_authn_v3.JwtProvider, error) { +// makeComputedProviderName is used to create names for unique provider per permission +// This is to stop jwt claims cross validation across permissions/providers. +// +// eg. If Permission x is the 3rd permission and has a provider of original name okta +// this function will return okta_3 as the computed provider name +func makeComputedProviderName(name string, perm *structs.IntentionPermission, idx int) string { + if perm == nil { + return name + } + return fmt.Sprintf("%s_%d", name, idx) +} + +// buildPayloadInMetadataKey is used to create a unique payload key per provider/permissions. +// This is to ensure claims are validated/forwarded specifically under the right permission/path +// and ensure we don't accidentally validate claims from different permissions/providers. +// +// eg. With a provider named okta, the second permission in permission list will have a provider of: +// okta_2 and a payload key of: jwt_payload_okta_2. Whereas an okta provider with no specific permission +// will have a payload key of: jwt_payload_okta +func buildPayloadInMetadataKey(providerName string, perm *structs.IntentionPermission, idx int) string { + return fmt.Sprintf("%s_%s", jwtMetadataKeyPrefix, makeComputedProviderName(providerName, perm, idx)) +} + +func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry, metadataKeySuffix string) (*envoy_http_jwt_authn_v3.JwtProvider, error) { envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{ - Issuer: p.Issuer, - Audiences: p.Audiences, + Issuer: p.Issuer, + Audiences: p.Audiences, + PayloadInMetadata: buildPayloadInMetadataKey(metadataKeySuffix, nil, 0), } if p.Forwarding != nil { @@ -216,7 +259,7 @@ func buildJWTRetryPolicy(r *structs.JWKSRetryPolicy) *envoy_core_v3.RetryPolicy return &pol } -func buildRouteRule(provider *structs.IntentionJWTProvider, perm *structs.IntentionPermission, defaultPrefix string) *envoy_http_jwt_authn_v3.RequirementRule { +func buildRouteRule(provider *structs.IntentionJWTProvider, perm *structs.IntentionPermission, defaultPrefix string, permIdx int) *envoy_http_jwt_authn_v3.RequirementRule { rule := &envoy_http_jwt_authn_v3.RequirementRule{ Match: &envoy_route_v3.RouteMatch{ PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: defaultPrefix}, @@ -224,7 +267,7 @@ func buildRouteRule(provider *structs.IntentionJWTProvider, perm *structs.Intent RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: provider.Name, + ProviderName: makeComputedProviderName(provider.Name, perm, permIdx), }, }, }, diff --git a/agent/xds/jwt_authn_test.go b/agent/xds/jwt_authn_test.go index 1b6179fa0b..589421e8c0 100644 --- a/agent/xds/jwt_authn_test.go +++ b/agent/xds/jwt_authn_test.go @@ -65,21 +65,21 @@ var ( pWithOktaProvider = &structs.IntentionPermission{ Action: structs.IntentionActionAllow, HTTP: &structs.IntentionHTTPPermission{ - PathPrefix: "/some-special-path", + PathPrefix: "some-special-path", }, JWT: oktaIntention, } pWithMultiProviders = &structs.IntentionPermission{ Action: structs.IntentionActionAllow, HTTP: &structs.IntentionHTTPPermission{ - PathPrefix: "/some-special-path", + PathPrefix: "some-special-path", }, JWT: multiProviderIntentions, } pWithNoJWT = &structs.IntentionPermission{ Action: structs.IntentionActionAllow, HTTP: &structs.IntentionHTTPPermission{ - PathPrefix: "/some-special-path", + PathPrefix: "some-special-path", }, } fullRetryPolicy = &structs.JWKSRetryPolicy{ @@ -206,51 +206,123 @@ func TestMakeJWTAUTHFilters(t *testing.T) { } } -func TestCollectJWTRequirements(t *testing.T) { - var ( - emptyReq = []*structs.IntentionJWTProvider{} - oneReq = []*structs.IntentionJWTProvider{&oktaProvider} - multiReq = append(oneReq, &auth0Provider) - ) - +func TestMakeComputedProviderName(t *testing.T) { tests := map[string]struct { - intention *structs.Intention - expected []*structs.IntentionJWTProvider + name string + perm *structs.IntentionPermission + idx int + expected string }{ - "empty-top-level-jwt-and-empty-permissions": { - intention: makeTestIntention(t, ixnOpts{src: "web"}), - expected: emptyReq, + "no-permissions": { + name: "okta", + idx: 0, + expected: "okta", }, - "top-level-jwt-and-empty-permissions": { - intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}), - expected: oneReq, - }, - "multi-top-level-jwt-and-empty-permissions": { - intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}), - expected: multiReq, - }, - "top-level-jwt-and-one-jwt-permission": { - intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}), - expected: multiReq, - }, - "top-level-jwt-and-multi-jwt-permissions": { - intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}), - expected: append(multiReq, &fakeProvider), - }, - "empty-top-level-jwt-and-one-jwt-permission": { - intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}), - expected: oneReq, - }, - "empty-top-level-jwt-and-multi-jwt-permission": { - intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}), - expected: multiReq, + "exact-path-permission": { + name: "auth0", + perm: &structs.IntentionPermission{ + HTTP: &structs.IntentionHTTPPermission{ + PathExact: "admin", + }, + }, + idx: 5, + expected: "auth0_5", }, } for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { - reqs := collectJWTRequirements(tt.intention) + reqs := makeComputedProviderName(tt.name, tt.perm, tt.idx) + require.Equal(t, reqs, tt.expected) + }) + } +} + +func TestBuildPayloadInMetadataKey(t *testing.T) { + tests := map[string]struct { + name string + perm *structs.IntentionPermission + permIdx int + expected string + }{ + "no-permissions": { + name: "okta", + expected: "jwt_payload_okta", + }, + "path-prefix-permission": { + name: "auth0", + perm: &structs.IntentionPermission{ + HTTP: &structs.IntentionHTTPPermission{ + PathPrefix: "admin", + }, + }, + permIdx: 4, + expected: "jwt_payload_auth0_4", + }, + } + + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + reqs := buildPayloadInMetadataKey(tt.name, tt.perm, tt.permIdx) + require.Equal(t, reqs, tt.expected) + }) + } +} + +func TestCollectJWTAuthnProviders(t *testing.T) { + tests := map[string]struct { + intention *structs.Intention + expected []*jwtAuthnProvider + }{ + "empty-top-level-jwt-and-empty-permissions": { + intention: makeTestIntention(t, ixnOpts{src: "web"}), + expected: []*jwtAuthnProvider{}, + }, + "top-level-jwt-and-empty-permissions": { + intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}), + expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: oktaProvider.Name}}, + }, + "multi-top-level-jwt-and-empty-permissions": { + intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}), + expected: []*jwtAuthnProvider{ + {Provider: &oktaProvider, ComputedName: oktaProvider.Name}, + {Provider: &auth0Provider, ComputedName: auth0Provider.Name}, + }, + }, + "top-level-jwt-and-one-jwt-permission": { + intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}), + expected: []*jwtAuthnProvider{ + {Provider: &auth0Provider, ComputedName: auth0Provider.Name}, + {Provider: &oktaProvider, ComputedName: "okta_0"}, + }, + }, + "top-level-jwt-and-multi-jwt-permissions": { + intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}), + expected: []*jwtAuthnProvider{ + {Provider: &fakeProvider, ComputedName: fakeProvider.Name}, + {Provider: &oktaProvider, ComputedName: "okta_0"}, + {Provider: &auth0Provider, ComputedName: "auth0_0"}, + }, + }, + "empty-top-level-jwt-and-one-jwt-permission": { + intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}), + expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: "okta_0"}}, + }, + "empty-top-level-jwt-and-multi-jwt-permission": { + intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}), + expected: []*jwtAuthnProvider{ + {Provider: &oktaProvider, ComputedName: "okta_0"}, + {Provider: &auth0Provider, ComputedName: "auth0_0"}, + }, + }, + } + + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + reqs := collectJWTAuthnProviders(tt.intention) require.ElementsMatch(t, reqs, tt.expected) }) } @@ -259,27 +331,32 @@ func TestCollectJWTRequirements(t *testing.T) { func TestGetPermissionsProviders(t *testing.T) { tests := map[string]struct { perms []*structs.IntentionPermission - expected []*structs.IntentionJWTProvider + expected []*jwtAuthnProvider }{ "empty-permissions": { perms: []*structs.IntentionPermission{}, - expected: []*structs.IntentionJWTProvider{}, + expected: []*jwtAuthnProvider{}, }, "nil-permissions": { perms: nil, - expected: []*structs.IntentionJWTProvider{}, + expected: []*jwtAuthnProvider{}, }, "permissions-with-no-jwt": { perms: []*structs.IntentionPermission{pWithNoJWT}, - expected: []*structs.IntentionJWTProvider{}, + expected: []*jwtAuthnProvider{}, }, "permissions-with-one-jwt": { - perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT}, - expected: []*structs.IntentionJWTProvider{&oktaProvider}, + perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT}, + expected: []*jwtAuthnProvider{ + {Provider: &oktaProvider, ComputedName: "okta_0"}, + }, }, "permissions-with-multiple-jwt": { - perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT}, - expected: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider}, + perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT}, + expected: []*jwtAuthnProvider{ + {Provider: &auth0Provider, ComputedName: "auth0_0"}, + {Provider: &oktaProvider, ComputedName: "okta_0"}, + }, }, } @@ -338,6 +415,7 @@ func TestBuildJWTProviderConfig(t *testing.T) { Issuer: fullCE.Issuer, Audiences: fullCE.Audiences, ForwardPayloadHeader: "user-token", + PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), PadForwardPayloadHeader: false, Forward: true, JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_LocalJwks{ @@ -353,8 +431,9 @@ func TestBuildJWTProviderConfig(t *testing.T) { "entry-with-remote-jwks": { ce: &ceRemoteJWKS, expected: &envoy_http_jwt_authn_v3.JwtProvider{ - Issuer: fullCE.Issuer, - Audiences: fullCE.Audiences, + Issuer: fullCE.Issuer, + Audiences: fullCE.Audiences, + PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_RemoteJwks{ RemoteJwks: &envoy_http_jwt_authn_v3.RemoteJwks{ HttpUri: &envoy_core_v3.HttpUri{ @@ -374,7 +453,7 @@ func TestBuildJWTProviderConfig(t *testing.T) { for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { - res, err := buildJWTProviderConfig(tt.ce) + res, err := buildJWTProviderConfig(tt.ce, tt.ce.GetName()) if tt.expectedError != "" { require.Error(t, err) @@ -585,7 +664,7 @@ func TestBuildRouteRule(t *testing.T) { RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: oktaProvider.Name, + ProviderName: makeComputedProviderName(oktaProvider.Name, pWithMultiProviders, 0), }, }, }, @@ -602,7 +681,7 @@ func TestBuildRouteRule(t *testing.T) { RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: oktaProvider.Name, + ProviderName: makeComputedProviderName(oktaProvider.Name, pWithExactPath, 0), }, }, }, @@ -619,7 +698,7 @@ func TestBuildRouteRule(t *testing.T) { RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: oktaProvider.Name, + ProviderName: makeComputedProviderName(oktaProvider.Name, pWithRegex, 0), }, }, }, @@ -630,7 +709,7 @@ func TestBuildRouteRule(t *testing.T) { for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { - res := buildRouteRule(tt.provider, tt.perm, tt.route) + res := buildRouteRule(tt.provider, tt.perm, tt.route, 0) require.Equal(t, res, tt.expected) }) } diff --git a/agent/xds/rbac.go b/agent/xds/rbac.go index 9648d3edc5..4cb77ad7f0 100644 --- a/agent/xds/rbac.go +++ b/agent/xds/rbac.go @@ -232,16 +232,40 @@ func intentionToIntermediateRBACForm( rixn.Source.TrustDomain = bundle.TrustDomain } + if isHTTP && ixn.JWT != nil { + var c []*JWTInfo + for _, prov := range ixn.JWT.Providers { + if len(prov.VerifyClaims) > 0 { + c = append(c, makeJWTInfos(prov, nil, 0)) + } + } + if len(c) > 0 { + rixn.jwtInfos = c + } + } + if len(ixn.Permissions) > 0 { if isHTTP { rixn.Action = intentionActionLayer7 rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions)) - for _, perm := range ixn.Permissions { - rixn.Permissions = append(rixn.Permissions, &rbacPermission{ + for k, perm := range ixn.Permissions { + rbacPerm := rbacPermission{ Definition: perm, Action: intentionActionFromString(perm.Action), Perm: convertPermission(perm), - }) + } + if perm.JWT != nil { + var c []*JWTInfo + for _, prov := range perm.JWT.Providers { + if len(prov.VerifyClaims) > 0 { + c = append(c, makeJWTInfos(prov, perm, k)) + } + } + if len(c) > 0 { + rbacPerm.jwtInfos = c + } + } + rixn.Permissions = append(rixn.Permissions, &rbacPerm) } } else { // In case L7 intentions slip through to here, treat them as deny intentions. @@ -254,8 +278,17 @@ func intentionToIntermediateRBACForm( return rixn } +func makeJWTInfos(p *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permKey int) *JWTInfo { + return &JWTInfo{Claims: p.VerifyClaims, MetadataPayloadKey: buildPayloadInMetadataKey(p.Name, perm, permKey)} +} + type intentionAction int +type JWTInfo struct { + Claims []*structs.IntentionJWTClaimVerification + MetadataPayloadKey string +} + const ( intentionActionDeny intentionAction = iota intentionActionAllow @@ -294,6 +327,11 @@ type rbacIntention struct { Permissions []*rbacPermission Precedence int + // JWTInfo is used to track intentions' JWT information + // This information is used to update HTTP filters for + // JWT Payload & claims validation + jwtInfos []*JWTInfo + // Skip is field used to indicate that this intention can be deleted in the // final pass. Items marked as true should generally not escape the method // that marked them. @@ -366,6 +404,11 @@ type rbacPermission struct { Perm *envoy_rbac_v3.Permission NotPerms []*envoy_rbac_v3.Permission + // JWTInfo is used to track intentions' JWT information + // This information is used to update HTTP filters for + // JWT Payload & claims validation + jwtInfos []*JWTInfo + // Skip is field used to indicate that this permission can be deleted in // the final pass. Items marked as true should generally not escape the // method that marked them. @@ -531,6 +574,10 @@ func makeRBACRules( var principalsL4 []*envoy_rbac_v3.Principal for i, rbacIxn := range rbacIxns { + var infos []*JWTInfo + if isHTTP { + infos = collectJWTInfos(rbacIxn) + } if rbacIxn.Action == intentionActionLayer7 { if len(rbacIxn.Permissions) == 0 { panic("invalid state: L7 intention has no permissions") @@ -539,9 +586,14 @@ func makeRBACRules( panic("invalid state: L7 permissions present for TCP service") } + rbacPrincipals := optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal}) + if len(infos) > 0 { + claimsPrincipal := jwtInfosToPrincipals(infos) + rbacPrincipals = combineBasePrincipalWithJWTPrincipals(rbacPrincipals, claimsPrincipal) + } // For L7: we should generate one Policy per Principal and list all of the Permissions policy := &envoy_rbac_v3.Policy{ - Principals: optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal}), + Principals: rbacPrincipals, Permissions: make([]*envoy_rbac_v3.Permission, 0, len(rbacIxn.Permissions)), } for _, perm := range rbacIxn.Permissions { @@ -551,6 +603,11 @@ func makeRBACRules( } else { // For L4: we should generate one big Policy listing all Principals principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal) + // Append JWT principals to list of principals + if len(infos) > 0 { + claimsPrincipal := jwtInfosToPrincipals(infos) + principalsL4 = combineBasePrincipalWithJWTPrincipals(principalsL4, claimsPrincipal) + } } } if len(principalsL4) > 0 { @@ -566,6 +623,109 @@ func makeRBACRules( return rbac } +// combineBasePrincipalWithJWTPrincipals ensure each RBAC/Network principal is associated with +// the JWT principal +func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, cp *envoy_rbac_v3.Principal) []*envoy_rbac_v3.Principal { + res := make([]*envoy_rbac_v3.Principal, 0) + + for _, principal := range p { + if principal != nil && cp != nil { + p := andPrincipals([]*envoy_rbac_v3.Principal{principal, cp}) + res = append(res, p) + } + } + return res +} + +// collectJWTInfos extracts all the collected JWTInfos top level infos +// and permission level infos and returns them as a single array +func collectJWTInfos(rbacIxn *rbacIntention) []*JWTInfo { + infos := make([]*JWTInfo, 0, len(rbacIxn.jwtInfos)) + + if len(rbacIxn.jwtInfos) > 0 { + infos = append(infos, rbacIxn.jwtInfos...) + } + for _, perm := range rbacIxn.Permissions { + infos = append(infos, perm.jwtInfos...) + } + + return infos +} + +func jwtInfosToPrincipals(c []*JWTInfo) *envoy_rbac_v3.Principal { + ps := make([]*envoy_rbac_v3.Principal, 0) + + for _, jwtInfo := range c { + if jwtInfo != nil { + for _, claim := range jwtInfo.Claims { + ps = append(ps, jwtClaimToPrincipal(claim, jwtInfo.MetadataPayloadKey)) + } + } + } + return orPrincipals(ps) +} + +// jwtClaimToPrincipal takes in a payloadkey which is the metadata key. This key is generated by using provider name, +// permission index with a jwt_payload prefix. See buildPayloadInMetadataKey in agent/xds/jwt_authn.go +// +// This uniquely generated payloadKey is the first segment in the path to validate the JWT claims. The subsequent segments +// come from the Path included in the IntentionJWTClaimVerification param. +func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Principal { + segments := pathToSegments(c.Path, payloadKey) + + return &envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: segments, + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: c.Value, + }, + }, + }, + }, + }, + }, + } +} + +// pathToSegments generates an array of MetadataMatcher_PathSegment that starts with the payloadkey +// and is followed by all existing strings in the path. +// +// eg. calling: pathToSegments([]string{"perms", "roles"}, "jwt_payload_okta") should return the following: +// +// []*envoy_matcher_v3.MetadataMatcher_PathSegment{ +// { +// Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "jwt_payload_okta"}, +// }, +// { +// Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "perms"}, +// }, +// { +// Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "roles"}, +// }, +// }, +func pathToSegments(paths []string, payloadKey string) []*envoy_matcher_v3.MetadataMatcher_PathSegment { + + segments := make([]*envoy_matcher_v3.MetadataMatcher_PathSegment, 0, len(paths)) + segments = append(segments, makeSegment(payloadKey)) + + for _, p := range paths { + segments = append(segments, makeSegment(p)) + } + + return segments +} + +func makeSegment(key string) *envoy_matcher_v3.MetadataMatcher_PathSegment { + return &envoy_matcher_v3.MetadataMatcher_PathSegment{ + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: key}, + } +} + func optimizePrincipals(orig []*envoy_rbac_v3.Principal) []*envoy_rbac_v3.Principal { // If they are all ORs, then OR them together. var orIds []*envoy_rbac_v3.Principal diff --git a/agent/xds/rbac_test.go b/agent/xds/rbac_test.go index 421f5b7a90..76f4467bff 100644 --- a/agent/xds/rbac_test.go +++ b/agent/xds/rbac_test.go @@ -484,6 +484,17 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { ixn.Permissions = perms return ixn } + testIntentionWithJWT := func(src string, action structs.IntentionAction, jwt *structs.IntentionJWTRequirement, perms ...*structs.IntentionPermission) *structs.Intention { + ixn := testIntention(t, src, "api", action) + ixn.JWT = jwt + ixn.Action = action + if perms != nil { + ixn.Permissions = perms + ixn.Action = "" + } + + return ixn + } testPeerTrustBundle := []*pbpeering.PeeringTrustBundle{ { PeerName: "peer1", @@ -506,6 +517,28 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { PathPrefix: "/", }, } + oktaWithClaims = structs.IntentionJWTProvider{ + Name: "okta", + VerifyClaims: []*structs.IntentionJWTClaimVerification{ + {Path: []string{"roles"}, Value: "testing"}, + }, + } + auth0WithClaims = structs.IntentionJWTProvider{ + Name: "auth0", + VerifyClaims: []*structs.IntentionJWTClaimVerification{ + {Path: []string{"perms", "role"}, Value: "admin"}, + }, + } + jwtRequirement = &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{ + &oktaWithClaims, + }, + } + auth0Requirement = &structs.IntentionJWTRequirement{ + Providers: []*structs.IntentionJWTProvider{ + &auth0WithClaims, + }, + } permDenySlashPrefix = &structs.IntentionPermission{ Action: structs.IntentionActionDeny, HTTP: &structs.IntentionHTTPPermission{ @@ -804,6 +837,70 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { ), ), }, + // ========= JWTAuthn Filter checks + "top-level-jwt-no-permissions": { + intentionDefaultAllow: false, + intentions: sorted( + testIntentionWithJWT("web", structs.IntentionActionAllow, jwtRequirement), + ), + }, + "empty-top-level-jwt-with-one-permission": { + intentionDefaultAllow: false, + intentions: sorted( + testIntentionWithJWT("web", structs.IntentionActionAllow, nil, &structs.IntentionPermission{ + Action: structs.IntentionActionAllow, + HTTP: &structs.IntentionHTTPPermission{ + PathPrefix: "some-path", + }, + JWT: jwtRequirement, + }), + ), + }, + "top-level-jwt-with-one-permission": { + intentionDefaultAllow: false, + intentions: sorted( + testIntentionWithJWT("web", + structs.IntentionActionAllow, + jwtRequirement, + &structs.IntentionPermission{ + Action: structs.IntentionActionAllow, + HTTP: &structs.IntentionHTTPPermission{ + PathExact: "/v1/secret", + }, + JWT: auth0Requirement, + }, + &structs.IntentionPermission{ + Action: structs.IntentionActionAllow, + HTTP: &structs.IntentionHTTPPermission{ + PathExact: "/v1/admin", + }, + }, + ), + ), + }, + "top-level-jwt-with-multiple-permissions": { + intentionDefaultAllow: false, + intentions: sorted( + testIntentionWithJWT("web", + structs.IntentionActionAllow, + jwtRequirement, + &structs.IntentionPermission{ + Action: structs.IntentionActionAllow, + HTTP: &structs.IntentionHTTPPermission{ + PathExact: "/v1/secret", + }, + JWT: auth0Requirement, + }, + &structs.IntentionPermission{ + Action: structs.IntentionActionAllow, + HTTP: &structs.IntentionHTTPPermission{ + PathExact: "/v1/admin", + }, + JWT: auth0Requirement, + }, + ), + ), + }, } testLocalInfo := rbacLocalInfo{ @@ -1060,3 +1157,142 @@ func TestSpiffeMatcher(t *testing.T) { }) } } + +func TestPathToSegments(t *testing.T) { + tests := map[string]struct { + key string + paths []string + expected []*envoy_matcher_v3.MetadataMatcher_PathSegment + }{ + "single-path": { + key: "jwt_payload_okta", + paths: []string{"perms"}, + expected: []*envoy_matcher_v3.MetadataMatcher_PathSegment{ + { + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "jwt_payload_okta"}, + }, + { + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "perms"}, + }, + }, + }, + "multi-paths": { + key: "jwt_payload_okta", + paths: []string{"perms", "roles"}, + expected: []*envoy_matcher_v3.MetadataMatcher_PathSegment{ + { + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "jwt_payload_okta"}, + }, + { + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "perms"}, + }, + { + Segment: &envoy_matcher_v3.MetadataMatcher_PathSegment_Key{Key: "roles"}, + }, + }, + }, + } + + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + segments := pathToSegments(tt.paths, tt.key) + require.ElementsMatch(t, segments, tt.expected) + }) + } +} + +func TestJwtClaimToPrincipal(t *testing.T) { + var ( + firstClaim = structs.IntentionJWTClaimVerification{ + Path: []string{"perms"}, + Value: "admin", + } + secondClaim = structs.IntentionJWTClaimVerification{ + Path: []string{"passage"}, + Value: "secret", + } + payloadKey = "dummy-key" + firstPrincipal = envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: pathToSegments(firstClaim.Path, payloadKey), + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: firstClaim.Value, + }, + }, + }, + }, + }, + }, + } + secondPrincipal = envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: pathToSegments(secondClaim.Path, "second-key"), + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: secondClaim.Value, + }, + }, + }, + }, + }, + }, + } + ) + tests := map[string]struct { + jwtInfos []*JWTInfo + expected *envoy_rbac_v3.Principal + }{ + "single-jwt-info": { + jwtInfos: []*JWTInfo{ + { + Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, + MetadataPayloadKey: payloadKey, + }, + }, + expected: &envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_OrIds{ + OrIds: &envoy_rbac_v3.Principal_Set{ + Ids: []*envoy_rbac_v3.Principal{&firstPrincipal}, + }, + }, + }, + }, + "multiple-jwt-info": { + jwtInfos: []*JWTInfo{ + { + Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, + MetadataPayloadKey: payloadKey, + }, + { + Claims: []*structs.IntentionJWTClaimVerification{&secondClaim}, + MetadataPayloadKey: "second-key", + }, + }, + expected: &envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_OrIds{ + OrIds: &envoy_rbac_v3.Principal_Set{ + Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal}, + }, + }, + }, + }, + } + + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + principal := jwtInfosToPrincipals(tt.jwtInfos) + require.Equal(t, principal, tt.expected) + }) + } +} diff --git a/agent/xds/testdata/jwt_authn/intention-with-path.golden b/agent/xds/testdata/jwt_authn/intention-with-path.golden index 9f0575f79a..306ecad5f6 100644 --- a/agent/xds/testdata/jwt_authn/intention-with-path.golden +++ b/agent/xds/testdata/jwt_authn/intention-with-path.golden @@ -3,8 +3,9 @@ "typedConfig": { "@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication", "providers": { - "okta": { + "okta_0": { "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta_0", "remoteJwks": { "httpUri": { "uri": "https://example-okta.com/.well-known/jwks.json", @@ -20,10 +21,10 @@ "rules": [ { "match": { - "prefix": "/some-special-path" + "prefix": "some-special-path" }, "requires": { - "providerName": "okta" + "providerName": "okta_0" } } ] diff --git a/agent/xds/testdata/jwt_authn/local-provider.golden b/agent/xds/testdata/jwt_authn/local-provider.golden index 8d660d3a6b..9efda0042b 100644 --- a/agent/xds/testdata/jwt_authn/local-provider.golden +++ b/agent/xds/testdata/jwt_authn/local-provider.golden @@ -5,6 +5,7 @@ "providers": { "okta": { "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta", "localJwks": { "inlineString": "{\"keys\": [{\n \"crv\": \"P-256\",\n \"key_ops\": [\n \"verify\"\n ],\n \"kty\": \"EC\",\n \"x\": \"Wc9uZuPaB7Kh2FMc9wtJjRe8XD4yT2AYNAAkrYbVjuw\",\n \"y\": \"68hRTJiJNOwtrh4EoPXeVnRuH7hiSDJ_lmbbjfDfWq0\",\n \"alg\": \"ES256\",\n \"use\": \"sig\",\n \"kid\": \"ac1e8f90eddf61c429c61ca05b4f2e07\"\n}]}" } diff --git a/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden b/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden index f91d170433..feb1d6012e 100644 --- a/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden +++ b/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden @@ -5,6 +5,21 @@ "providers": { "okta": { "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta", + "remoteJwks": { + "httpUri": { + "uri": "https://example-okta.com/.well-known/jwks.json", + "cluster": "jwks_cluster", + "timeout": "1s" + }, + "asyncFetch": { + "fastListener": true + } + } + }, + "okta_0": { + "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta_0", "remoteJwks": { "httpUri": { "uri": "https://example-okta.com/.well-known/jwks.json", @@ -18,6 +33,7 @@ }, "auth0": { "issuer": "another-issuer", + "payloadInMetadata": "jwt_payload_auth0", "remoteJwks": { "httpUri": { "uri": "https://example-auth0.com/.well-known/jwks.json", @@ -33,10 +49,10 @@ "rules": [ { "match": { - "prefix": "/some-special-path" + "prefix": "some-special-path" }, "requires": { - "providerName": "okta" + "providerName": "okta_0" } }, { diff --git a/agent/xds/testdata/jwt_authn/remote-provider.golden b/agent/xds/testdata/jwt_authn/remote-provider.golden index bdfa07a543..b84e1ea102 100644 --- a/agent/xds/testdata/jwt_authn/remote-provider.golden +++ b/agent/xds/testdata/jwt_authn/remote-provider.golden @@ -5,6 +5,7 @@ "providers": { "okta": { "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta", "remoteJwks": { "httpUri": { "uri": "https://example-okta.com/.well-known/jwks.json", diff --git a/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden b/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden index e8b147b040..42a609470d 100644 --- a/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden +++ b/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden @@ -5,6 +5,21 @@ "providers": { "okta": { "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta", + "remoteJwks": { + "httpUri": { + "uri": "https://example-okta.com/.well-known/jwks.json", + "cluster": "jwks_cluster", + "timeout": "1s" + }, + "asyncFetch": { + "fastListener": true + } + } + }, + "okta_0": { + "issuer": "test-issuer", + "payloadInMetadata": "jwt_payload_okta_0", "remoteJwks": { "httpUri": { "uri": "https://example-okta.com/.well-known/jwks.json", @@ -20,10 +35,10 @@ "rules": [ { "match": { - "prefix": "/some-special-path" + "prefix": "some-special-path" }, "requires": { - "providerName": "okta" + "providerName": "okta_0" } }, { diff --git a/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden new file mode 100644 index 0000000000..cd5c35bab2 --- /dev/null +++ b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden @@ -0,0 +1,59 @@ +{ + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + "policies": { + "consul-intentions-layer7-0": { + "permissions": [ + { + "urlPath": { + "path": { + "prefix": "some-path" + } + } + } + ], + "principals": [ + { + "andIds": { + "ids": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": {}, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + }, + { + "orIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta_0"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } + } + ] + } + } + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission.golden b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission.golden new file mode 100644 index 0000000000..92c7f921ad --- /dev/null +++ b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission.golden @@ -0,0 +1,10 @@ +{ + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + + }, + "statPrefix": "connect_authz" + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden new file mode 100644 index 0000000000..35b3792e66 --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden @@ -0,0 +1,57 @@ +{ + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + "policies": { + "consul-intentions-layer4": { + "permissions": [ + { + "any": true + } + ], + "principals": [ + { + "andIds": { + "ids": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": { + + }, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + }, + { + "orIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } + } + ] + } + } + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-no-permissions.golden b/agent/xds/testdata/rbac/top-level-jwt-no-permissions.golden new file mode 100644 index 0000000000..3fc78b2955 --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-no-permissions.golden @@ -0,0 +1,32 @@ +{ + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + "policies": { + "consul-intentions-layer4": { + "permissions": [ + { + "any": true + } + ], + "principals": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": { + + }, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + } + ] + } + } + }, + "statPrefix": "connect_authz" + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden new file mode 100644 index 0000000000..409a3a4bd6 --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden @@ -0,0 +1,113 @@ +{ + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + "policies": { + "consul-intentions-layer7-0": { + "permissions": [ + { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + }, + { + "andRules": { + "rules": [ + { + "urlPath": { + "path": { + "exact": "/v1/admin" + } + } + }, + { + "notRule": { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + } + } + ] + } + } + ], + "principals": [ + { + "andIds": { + "ids": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": { + + }, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + }, + { + "orIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_1"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } + } + ] + } + } + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions.golden b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions.golden new file mode 100644 index 0000000000..92c7f921ad --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions.golden @@ -0,0 +1,10 @@ +{ + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + + }, + "statPrefix": "connect_authz" + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden new file mode 100644 index 0000000000..edf027f3e0 --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden @@ -0,0 +1,98 @@ +{ + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + "policies": { + "consul-intentions-layer7-0": { + "permissions": [ + { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + }, + { + "andRules": { + "rules": [ + { + "urlPath": { + "path": { + "exact": "/v1/admin" + } + } + }, + { + "notRule": { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + } + } + ] + } + } + ], + "principals": [ + { + "andIds": { + "ids": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": { + + }, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + }, + { + "orIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } + } + ] + } + } + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-one-permission.golden b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission.golden new file mode 100644 index 0000000000..92c7f921ad --- /dev/null +++ b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission.golden @@ -0,0 +1,10 @@ +{ + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + + }, + "statPrefix": "connect_authz" + } +} \ No newline at end of file