Use JWT-auth filter in metadata mode & Delegate validation to RBAC filter

pull/18062/head
Ronald Ekambi 2023-07-07 16:15:17 -04:00
parent e7194787a7
commit 70536f5a38
16 changed files with 658 additions and 614 deletions

View File

@ -13,6 +13,7 @@ import (
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb" "google.golang.org/protobuf/types/known/wrapperspb"
) )
@ -22,129 +23,149 @@ const (
jwksClusterPrefix = "jwks_cluster" jwksClusterPrefix = "jwks_cluster"
) )
// This is an intermediate JWTProvider form used to associate // makeJWTAuthFilter builds jwt filter for envoy. It limits its use to referenced provider rather than every provider.
// unique payload keys to providers //
type jwtAuthnProvider struct { // Eg. If you have three providers: okta, auth0 and fusionAuth and only okta is referenced in your intentions, then this
ComputedName string // will create a jwt-auth filter containing just okta in the list of providers.
Provider *structs.IntentionJWTProvider func makeJWTAuthFilter(providerMap map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) {
}
func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) {
providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{} providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{}
var rules []*envoy_http_jwt_authn_v3.RequirementRule var jwtRequirements []*envoy_http_jwt_authn_v3.JwtRequirement
for _, intention := range intentions { for _, intention := range intentions {
if intention.JWT == nil && !hasJWTconfig(intention.Permissions) { if intention.JWT == nil && !hasJWTconfig(intention.Permissions) {
continue continue
} }
for _, jwtReq := range collectJWTAuthnProviders(intention) { for _, p := range collectJWTProviders(intention) {
if _, ok := providers[jwtReq.ComputedName]; ok { providerName := p.Name
if _, ok := providers[providerName]; ok {
continue continue
} }
jwtProvider, ok := pCE[jwtReq.Provider.Name] providerCE, ok := providerMap[providerName]
if !ok { if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Provider.Name) return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", providerName)
} }
// If intention permissions use HTTP-match criteria with
// VerifyClaims, then generate a clone of the jwt provider with a envoyCfg, err := buildJWTProviderConfig(providerCE)
// 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 { if err != nil {
return nil, err return nil, err
} }
providers[jwtReq.ComputedName] = envoyCfg providers[providerName] = envoyCfg
} reqs := providerToJWTRequirement(providerCE)
jwtRequirements = append(jwtRequirements, reqs)
for k, perm := range intention.Permissions {
if perm.JWT == nil {
continue
}
for _, prov := range perm.JWT.Providers {
rule := buildRouteRule(prov, perm, "/", k)
rules = append(rules, rule)
} }
} }
if intention.JWT != nil { if len(jwtRequirements) == 0 {
for _, provider := range intention.JWT.Providers { //do not add jwt_authn filter when intentions don't have JWTs
// The top-level provider applies to all requests.
rule := buildRouteRule(provider, nil, "/", 0)
rules = append(rules, rule)
}
}
}
if len(intentions) == 0 && len(providers) == 0 {
//do not add jwt_authn filter when intentions don't have JWT
return nil, nil return nil, nil
} }
cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{ cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{
Providers: providers, Providers: providers,
Rules: rules, Rules: []*envoy_http_jwt_authn_v3.RequirementRule{
{
Match: &envoy_route_v3.RouteMatch{
PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: "/"},
},
RequirementType: makeJWTRequirementRule(andJWTRequirements(jwtRequirements)),
},
},
} }
return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg) return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg)
} }
func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider { func makeJWTRequirementRule(r *envoy_http_jwt_authn_v3.JwtRequirement) *envoy_http_jwt_authn_v3.RequirementRule_Requires {
var reqs []*jwtAuthnProvider return &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: r,
}
}
// andJWTRequirements combines list of jwt requirements into a single jwt requirement.
func andJWTRequirements(reqs []*envoy_http_jwt_authn_v3.JwtRequirement) *envoy_http_jwt_authn_v3.JwtRequirement {
switch len(reqs) {
case 0:
return nil
case 1:
return reqs[0]
default:
return &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_RequiresAll{
RequiresAll: &envoy_http_jwt_authn_v3.JwtRequirementAndList{
Requirements: reqs,
},
},
}
}
}
// providerToJWTRequirement builds the envoy jwtRequirement.
//
// Note: since the rbac filter is in charge of making decisions of allow/denied, this
// requirement uses `allow_missing_or_failed` to ensure it is always satisfied.
func providerToJWTRequirement(provider *structs.JWTProviderConfigEntry) *envoy_http_jwt_authn_v3.JwtRequirement {
return &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_RequiresAny{
RequiresAny: &envoy_http_jwt_authn_v3.JwtRequirementOrList{
Requirements: []*envoy_http_jwt_authn_v3.JwtRequirement{
{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{
ProviderName: provider.Name,
},
},
// We use allowMissingOrFailed to allow rbac filter to do the validation
{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_AllowMissingOrFailed{
AllowMissingOrFailed: &emptypb.Empty{},
},
},
},
},
},
}
}
// collectJWTProviders returns a list of all top level and permission level referenced providers.
func collectJWTProviders(i *structs.Intention) []*structs.IntentionJWTProvider {
// get permission level providers
reqs := getPermissionsProviders(i.Permissions)
if i.JWT != nil { if i.JWT != nil {
for _, prov := range i.JWT.Providers { // get top level providers
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, nil, 0)}) reqs = append(reqs, i.JWT.Providers...)
} }
}
reqs = append(reqs, getPermissionsProviders(i.Permissions)...)
return reqs return reqs
} }
func getPermissionsProviders(p []*structs.IntentionPermission) []*jwtAuthnProvider { func getPermissionsProviders(perms []*structs.IntentionPermission) []*structs.IntentionJWTProvider {
var reqs []*jwtAuthnProvider var reqs []*structs.IntentionJWTProvider
for k, perm := range p { for _, p := range perms {
if perm.JWT == nil { if p.JWT == nil {
continue continue
} }
for _, prov := range perm.JWT.Providers {
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, perm, k)}) reqs = append(reqs, p.JWT.Providers...)
}
} }
return reqs return reqs
} }
// makeComputedProviderName is used to create names for unique provider per permission // buildPayloadInMetadataKey is used to create a unique payload key per provider.
// This is to stop jwt claims cross validation across permissions/providers. // This is to ensure claims are validated/forwarded specifically under the right provider.
// The forwarded payload is used with other data (eg. service identity) by the RBAC filter
// to validate access to resource.
// //
// eg. If Permission x is the 3rd permission and has a provider of original name okta // eg. With a provider named okta will have a payload key of: jwt_payload_okta
// this function will return okta_3 as the computed provider name func buildPayloadInMetadataKey(providerName string) string {
func makeComputedProviderName(name string, perm *structs.IntentionPermission, idx int) string { return jwtMetadataKeyPrefix + "_" + providerName
if perm == nil {
return name
}
return fmt.Sprintf("%s_%d", name, idx)
} }
// buildPayloadInMetadataKey is used to create a unique payload key per provider/permissions. func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
// 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{ envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{
Issuer: p.Issuer, Issuer: p.Issuer,
Audiences: p.Audiences, Audiences: p.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(metadataKeySuffix, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(p.Name),
} }
if p.Forwarding != nil { if p.Forwarding != nil {
@ -262,43 +283,6 @@ func buildJWTRetryPolicy(r *structs.JWKSRetryPolicy) *envoy_core_v3.RetryPolicy
return &pol return &pol
} }
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},
},
RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{
ProviderName: makeComputedProviderName(provider.Name, perm, permIdx),
},
},
},
}
if perm != nil && perm.HTTP != nil {
if perm.HTTP.PathPrefix != "" {
rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Prefix{
Prefix: perm.HTTP.PathPrefix,
}
}
if perm.HTTP.PathExact != "" {
rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Path{
Path: perm.HTTP.PathExact,
}
}
if perm.HTTP.PathRegex != "" {
rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_SafeRegex{
SafeRegex: makeEnvoyRegexMatch(perm.HTTP.PathRegex),
}
}
}
return rule
}
func hasJWTconfig(p []*structs.IntentionPermission) bool { func hasJWTconfig(p []*structs.IntentionPermission) bool {
for _, perm := range p { for _, perm := range p {
if perm.JWT != nil { if perm.JWT != nil {

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" envoy_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -173,6 +172,10 @@ func TestMakeJWTAUTHFilters(t *testing.T) {
intentions structs.SimplifiedIntentions intentions structs.SimplifiedIntentions
provider map[string]*structs.JWTProviderConfigEntry provider map[string]*structs.JWTProviderConfigEntry
}{ }{
"no-provider": {
intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow})),
provider: nil,
},
"remote-provider": { "remote-provider": {
intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow, jwt: oktaIntention})), intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow, jwt: oktaIntention})),
provider: remoteCE, provider: remoteCE,
@ -206,123 +209,45 @@ func TestMakeJWTAUTHFilters(t *testing.T) {
} }
} }
func TestMakeComputedProviderName(t *testing.T) { func TestCollectJWTProviders(t *testing.T) {
tests := map[string]struct {
name string
perm *structs.IntentionPermission
idx int
expected string
}{
"no-permissions": {
name: "okta",
idx: 0,
expected: "okta",
},
"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 := 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 { tests := map[string]struct {
intention *structs.Intention intention *structs.Intention
expected []*jwtAuthnProvider expected []*structs.IntentionJWTProvider
}{ }{
"empty-top-level-jwt-and-empty-permissions": { "empty-top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web"}), intention: makeTestIntention(t, ixnOpts{src: "web"}),
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"top-level-jwt-and-empty-permissions": { "top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}),
expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: oktaProvider.Name}}, expected: []*structs.IntentionJWTProvider{&oktaProvider},
}, },
"multi-top-level-jwt-and-empty-permissions": { "multi-top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider},
{Provider: &oktaProvider, ComputedName: oktaProvider.Name},
{Provider: &auth0Provider, ComputedName: auth0Provider.Name},
},
}, },
"top-level-jwt-and-one-jwt-permission": { "top-level-jwt-and-one-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&auth0Provider, &oktaProvider},
{Provider: &auth0Provider, ComputedName: auth0Provider.Name},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
"top-level-jwt-and-multi-jwt-permissions": { "top-level-jwt-and-multi-jwt-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&fakeProvider, &oktaProvider, &auth0Provider},
{Provider: &fakeProvider, ComputedName: fakeProvider.Name},
{Provider: &oktaProvider, ComputedName: "okta_0"},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
},
}, },
"empty-top-level-jwt-and-one-jwt-permission": { "empty-top-level-jwt-and-one-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}), intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}),
expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: "okta_0"}}, expected: []*structs.IntentionJWTProvider{&oktaProvider},
}, },
"empty-top-level-jwt-and-multi-jwt-permission": { "empty-top-level-jwt-and-multi-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}), intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider},
{Provider: &oktaProvider, ComputedName: "okta_0"},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
},
}, },
} }
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
reqs := collectJWTAuthnProviders(tt.intention) reqs := collectJWTProviders(tt.intention)
require.ElementsMatch(t, reqs, tt.expected) require.ElementsMatch(t, reqs, tt.expected)
}) })
} }
@ -331,44 +256,36 @@ func TestCollectJWTAuthnProviders(t *testing.T) {
func TestGetPermissionsProviders(t *testing.T) { func TestGetPermissionsProviders(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
perms []*structs.IntentionPermission perms []*structs.IntentionPermission
expected []*jwtAuthnProvider expected []*structs.IntentionJWTProvider
}{ }{
"empty-permissions": { "empty-permissions": {
perms: []*structs.IntentionPermission{}, perms: []*structs.IntentionPermission{},
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"nil-permissions": { "nil-permissions": {
perms: nil, perms: nil,
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"permissions-with-no-jwt": { "permissions-with-no-jwt": {
perms: []*structs.IntentionPermission{pWithNoJWT}, perms: []*structs.IntentionPermission{pWithNoJWT},
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"permissions-with-one-jwt": { "permissions-with-one-jwt": {
perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT}, perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT},
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
"permissions-with-multiple-jwt": { "permissions-with-multiple-jwt": {
perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT}, perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT},
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&auth0Provider, &oktaProvider},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
} }
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Run("getPermissionsProviders", func(t *testing.T) {
p := getPermissionsProviders(tt.perms) p := getPermissionsProviders(tt.perms)
require.ElementsMatch(t, p, tt.expected) require.ElementsMatch(t, p, tt.expected)
}) })
})
} }
} }
@ -415,7 +332,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
Issuer: fullCE.Issuer, Issuer: fullCE.Issuer,
Audiences: fullCE.Audiences, Audiences: fullCE.Audiences,
ForwardPayloadHeader: "user-token", ForwardPayloadHeader: "user-token",
PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name),
PadForwardPayloadHeader: false, PadForwardPayloadHeader: false,
Forward: true, Forward: true,
JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_LocalJwks{ JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_LocalJwks{
@ -433,7 +350,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
expected: &envoy_http_jwt_authn_v3.JwtProvider{ expected: &envoy_http_jwt_authn_v3.JwtProvider{
Issuer: fullCE.Issuer, Issuer: fullCE.Issuer,
Audiences: fullCE.Audiences, Audiences: fullCE.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name),
JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_RemoteJwks{ JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_RemoteJwks{
RemoteJwks: &envoy_http_jwt_authn_v3.RemoteJwks{ RemoteJwks: &envoy_http_jwt_authn_v3.RemoteJwks{
HttpUri: &envoy_core_v3.HttpUri{ HttpUri: &envoy_core_v3.HttpUri{
@ -453,7 +370,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
res, err := buildJWTProviderConfig(tt.ce, tt.ce.GetName()) res, err := buildJWTProviderConfig(tt.ce)
if tt.expectedError != "" { if tt.expectedError != "" {
require.Error(t, err) require.Error(t, err)
@ -620,104 +537,6 @@ func TestBuildJWTRetryPolicy(t *testing.T) {
} }
} }
func TestBuildRouteRule(t *testing.T) {
var (
pWithExactPath = &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathExact: "/exact-match",
},
}
pWithRegex = &structs.IntentionPermission{
Action: structs.IntentionActionAllow,
HTTP: &structs.IntentionHTTPPermission{
PathRegex: "p([a-z]+)ch",
},
}
)
tests := map[string]struct {
provider *structs.IntentionJWTProvider
perm *structs.IntentionPermission
route string
expected *envoy_http_jwt_authn_v3.RequirementRule
}{
"permission-nil": {
provider: &oktaProvider,
perm: nil,
route: "/my-route",
expected: &envoy_http_jwt_authn_v3.RequirementRule{
Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: "/my-route"}},
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,
},
},
},
},
},
"permission-with-path-prefix": {
provider: &oktaProvider,
perm: pWithOktaProvider,
route: "/my-route",
expected: &envoy_http_jwt_authn_v3.RequirementRule{
Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{
Prefix: pWithMultiProviders.HTTP.PathPrefix,
}},
RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{
ProviderName: makeComputedProviderName(oktaProvider.Name, pWithMultiProviders, 0),
},
},
},
},
},
"permission-with-exact-path": {
provider: &oktaProvider,
perm: pWithExactPath,
route: "/",
expected: &envoy_http_jwt_authn_v3.RequirementRule{
Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Path{
Path: pWithExactPath.HTTP.PathExact,
}},
RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{
ProviderName: makeComputedProviderName(oktaProvider.Name, pWithExactPath, 0),
},
},
},
},
},
"permission-with-regex": {
provider: &oktaProvider,
perm: pWithRegex,
route: "/",
expected: &envoy_http_jwt_authn_v3.RequirementRule{
Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_SafeRegex{
SafeRegex: makeEnvoyRegexMatch(pWithRegex.HTTP.PathRegex),
}},
RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: &envoy_http_jwt_authn_v3.JwtRequirement{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{
ProviderName: makeComputedProviderName(oktaProvider.Name, pWithRegex, 0),
},
},
},
},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
res := buildRouteRule(tt.provider, tt.perm, tt.route, 0)
require.Equal(t, res, tt.expected)
})
}
}
func TestHasJWTconfig(t *testing.T) { func TestHasJWTconfig(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
perms []*structs.IntentionPermission perms []*structs.IntentionPermission

View File

@ -1291,6 +1291,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
cfgSnap.ConnectProxy.InboundPeerTrustBundles, cfgSnap.ConnectProxy.InboundPeerTrustBundles,
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -1364,9 +1365,9 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
logger: s.Logger, logger: s.Logger,
} }
if useHTTPFilter { if useHTTPFilter {
jwtFilter, jwtFilterErr := makeJWTAuthFilter(cfgSnap.JWTProviders, cfgSnap.ConnectProxy.Intentions) jwtFilter, err := makeJWTAuthFilter(cfgSnap.JWTProviders, cfgSnap.ConnectProxy.Intentions)
if jwtFilterErr != nil { if err != nil {
return nil, jwtFilterErr return nil, err
} }
rbacFilter, err := makeRBACHTTPFilter( rbacFilter, err := makeRBACHTTPFilter(
cfgSnap.ConnectProxy.Intentions, cfgSnap.ConnectProxy.Intentions,
@ -1377,6 +1378,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
cfgSnap.ConnectProxy.InboundPeerTrustBundles, cfgSnap.ConnectProxy.InboundPeerTrustBundles,
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -1844,6 +1846,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg.
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -28,7 +28,10 @@ func makeRBACNetworkFilter(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
) (*envoy_listener_v3.Filter, error) { ) (*envoy_listener_v3.Filter, error) {
rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles) rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles, nil)
if err != nil {
return nil, err
}
cfg := &envoy_network_rbac_v3.RBAC{ cfg := &envoy_network_rbac_v3.RBAC{
StatPrefix: "connect_authz", StatPrefix: "connect_authz",
@ -42,8 +45,12 @@ func makeRBACHTTPFilter(
intentionDefaultAllow bool, intentionDefaultAllow bool,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
providerMap map[string]*structs.JWTProviderConfigEntry,
) (*envoy_http_v3.HttpFilter, error) { ) (*envoy_http_v3.HttpFilter, error) {
rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles) rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles, providerMap)
if err != nil {
return nil, err
}
cfg := &envoy_http_rbac_v3.RBAC{ cfg := &envoy_http_rbac_v3.RBAC{
Rules: rules, Rules: rules,
@ -56,7 +63,8 @@ func intentionListToIntermediateRBACForm(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle, trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle,
) []*rbacIntention { providerMap map[string]*structs.JWTProviderConfigEntry,
) ([]*rbacIntention, error) {
sort.Sort(structs.IntentionPrecedenceSorter(intentions)) sort.Sort(structs.IntentionPrecedenceSorter(intentions))
// Omit any lower-precedence intentions that share the same source. // Omit any lower-precedence intentions that share the same source.
@ -73,10 +81,13 @@ func intentionListToIntermediateRBACForm(
continue continue
} }
rixn := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle) rixn, err := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle, providerMap)
if err != nil {
return nil, err
}
rbacIxns = append(rbacIxns, rixn) rbacIxns = append(rbacIxns, rixn)
} }
return rbacIxns return rbacIxns, nil
} }
func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction, localInfo rbacLocalInfo) []*rbacIntention { func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction, localInfo rbacLocalInfo) []*rbacIntention {
@ -216,7 +227,8 @@ func intentionToIntermediateRBACForm(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
bundle *pbpeering.PeeringTrustBundle, bundle *pbpeering.PeeringTrustBundle,
) *rbacIntention { providerMap map[string]*structs.JWTProviderConfigEntry,
) (*rbacIntention, error) {
rixn := &rbacIntention{ rixn := &rbacIntention{
Source: rbacService{ Source: rbacService{
ServiceName: ixn.SourceServiceName(), ServiceName: ixn.SourceServiceName(),
@ -233,36 +245,41 @@ func intentionToIntermediateRBACForm(
} }
if isHTTP && ixn.JWT != nil { if isHTTP && ixn.JWT != nil {
var c []*JWTInfo var jwts []*JWTInfo
for _, prov := range ixn.JWT.Providers { for _, prov := range ixn.JWT.Providers {
if len(prov.VerifyClaims) > 0 { jwtProvider, ok := providerMap[prov.Name]
c = append(c, makeJWTInfos(prov, nil, 0))
if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name)
} }
jwts = append(jwts, newJWTInfo(prov, jwtProvider))
} }
if len(c) > 0 {
rixn.jwtInfos = c rixn.jwtInfos = jwts
}
} }
if len(ixn.Permissions) > 0 { if len(ixn.Permissions) > 0 {
if isHTTP { if isHTTP {
rixn.Action = intentionActionLayer7 rixn.Action = intentionActionLayer7
rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions)) rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions))
for k, perm := range ixn.Permissions { for _, perm := range ixn.Permissions {
rbacPerm := rbacPermission{ rbacPerm := rbacPermission{
Definition: perm, Definition: perm,
Action: intentionActionFromString(perm.Action), Action: intentionActionFromString(perm.Action),
Perm: convertPermission(perm), Perm: convertPermission(perm),
} }
if perm.JWT != nil { if perm.JWT != nil {
var c []*JWTInfo var jwts []*JWTInfo
for _, prov := range perm.JWT.Providers { for _, prov := range perm.JWT.Providers {
if len(prov.VerifyClaims) > 0 { jwtProvider, ok := providerMap[prov.Name]
c = append(c, makeJWTInfos(prov, perm, k)) if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name)
} }
jwts = append(jwts, newJWTInfo(prov, jwtProvider))
} }
if len(c) > 0 { if len(jwts) > 0 {
rbacPerm.jwtInfos = c rbacPerm.jwtInfos = jwts
} }
} }
rixn.Permissions = append(rixn.Permissions, &rbacPerm) rixn.Permissions = append(rixn.Permissions, &rbacPerm)
@ -275,18 +292,24 @@ func intentionToIntermediateRBACForm(
rixn.Action = intentionActionFromString(ixn.Action) rixn.Action = intentionActionFromString(ixn.Action)
} }
return rixn return rixn, nil
} }
func makeJWTInfos(p *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permKey int) *JWTInfo { func newJWTInfo(p *structs.IntentionJWTProvider, ce *structs.JWTProviderConfigEntry) *JWTInfo {
return &JWTInfo{Claims: p.VerifyClaims, MetadataPayloadKey: buildPayloadInMetadataKey(p.Name, perm, permKey)} return &JWTInfo{
Provider: p,
Issuer: ce.Issuer,
}
} }
type intentionAction int type intentionAction int
type JWTInfo struct { type JWTInfo struct {
Claims []*structs.IntentionJWTClaimVerification // Provider issuer
MetadataPayloadKey string // this information is coming from the config entry
Issuer string
// Provider is the intention provider
Provider *structs.IntentionJWTProvider
} }
const ( const (
@ -341,15 +364,14 @@ type rbacIntention struct {
} }
func (r *rbacIntention) FlattenPrincipal(localInfo rbacLocalInfo) *envoy_rbac_v3.Principal { func (r *rbacIntention) FlattenPrincipal(localInfo rbacLocalInfo) *envoy_rbac_v3.Principal {
var principal *envoy_rbac_v3.Principal
if !localInfo.expectXFCC { if !localInfo.expectXFCC {
return r.flattenPrincipalFromCert() principal = r.flattenPrincipalFromCert()
} else if r.Source.Peer == "" { } else if r.Source.Peer == "" {
// NOTE: ixnSourceMatches should enforce that all of Source and NotSources // NOTE: ixnSourceMatches should enforce that all of Source and NotSources
// are peered or not-peered, so we only need to look at the Source element. // are peered or not-peered, so we only need to look at the Source element.
return r.flattenPrincipalFromCert() // intention is not relevant to peering principal = r.flattenPrincipalFromCert() // intention is not relevant to peering
} } else {
// If this intention is an L7 peered one, then it is exclusively resolvable // If this intention is an L7 peered one, then it is exclusively resolvable
// using XFCC, rather than the TLS SAN field. // using XFCC, rather than the TLS SAN field.
fromXFCC := r.flattenPrincipalFromXFCC() fromXFCC := r.flattenPrincipalFromXFCC()
@ -357,12 +379,19 @@ func (r *rbacIntention) FlattenPrincipal(localInfo rbacLocalInfo) *envoy_rbac_v3
// Use of the XFCC one is gated on coming directly from our own gateways. // Use of the XFCC one is gated on coming directly from our own gateways.
gwIDPattern := makeSpiffeMeshGatewayPattern(localInfo.trustDomain, localInfo.partition) gwIDPattern := makeSpiffeMeshGatewayPattern(localInfo.trustDomain, localInfo.partition)
return andPrincipals([]*envoy_rbac_v3.Principal{ principal = andPrincipals([]*envoy_rbac_v3.Principal{
authenticatedPatternPrincipal(gwIDPattern), authenticatedPatternPrincipal(gwIDPattern),
fromXFCC, fromXFCC,
}) })
} }
if len(r.jwtInfos) == 0 {
return principal
}
return addJWTPrincipal(principal, r.jwtInfos)
}
func (r *rbacIntention) flattenPrincipalFromCert() *envoy_rbac_v3.Principal { func (r *rbacIntention) flattenPrincipalFromCert() *envoy_rbac_v3.Principal {
r.NotSources = simplifyNotSourceSlice(r.NotSources) r.NotSources = simplifyNotSourceSlice(r.NotSources)
@ -417,17 +446,47 @@ type rbacPermission struct {
ComputedPermission *envoy_rbac_v3.Permission ComputedPermission *envoy_rbac_v3.Permission
} }
// Flatten ensure the permission rules, not-rules, and jwt validation rules are merged into a single computed permission.
//
// Details on JWTInfo section:
// For each JWTInfo (AKA provider required), this builds 1 single permission that validates that the jwt has
// the right issuer (`iss`) field and validates the claims (if any).
//
// After generating a single permission per info, it combines all the info permissions into a single OrPermission.
// This orPermission is then attached to initial computed permission for jwt payload and claims validation.
func (p *rbacPermission) Flatten() *envoy_rbac_v3.Permission { func (p *rbacPermission) Flatten() *envoy_rbac_v3.Permission {
if len(p.NotPerms) == 0 { computedPermission := p.Perm
return p.Perm if len(p.NotPerms) == 0 && len(p.jwtInfos) == 0 {
return computedPermission
} }
if len(p.NotPerms) != 0 {
parts := make([]*envoy_rbac_v3.Permission, 0, len(p.NotPerms)+1) parts := make([]*envoy_rbac_v3.Permission, 0, len(p.NotPerms)+1)
parts = append(parts, p.Perm) parts = append(parts, p.Perm)
for _, notPerm := range p.NotPerms { for _, notPerm := range p.NotPerms {
parts = append(parts, notPermission(notPerm)) parts = append(parts, notPermission(notPerm))
} }
return andPermissions(parts) computedPermission = andPermissions(parts)
}
if len(p.jwtInfos) == 0 {
return computedPermission
}
var jwtPerms []*envoy_rbac_v3.Permission
for _, info := range p.jwtInfos {
payloadKey := buildPayloadInMetadataKey(info.Provider.Name)
claimsPermission := jwtInfosToPermission(info.Provider.VerifyClaims, payloadKey)
issuerPermission := segmentToPermission(pathToSegments([]string{"iss"}, payloadKey), info.Issuer)
perm := andPermissions([]*envoy_rbac_v3.Permission{
issuerPermission, claimsPermission,
})
jwtPerms = append(jwtPerms, perm)
}
jwtPerm := orPermissions(jwtPerms)
return andPermissions([]*envoy_rbac_v3.Permission{computedPermission, jwtPerm})
} }
// simplifyNotSourceSlice will collapse NotSources elements together if any element is // simplifyNotSourceSlice will collapse NotSources elements together if any element is
@ -526,7 +585,8 @@ func makeRBACRules(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
) *envoy_rbac_v3.RBAC { providerMap map[string]*structs.JWTProviderConfigEntry,
) (*envoy_rbac_v3.RBAC, error) {
// TODO(banks,rb): Implement revocation list checking? // TODO(banks,rb): Implement revocation list checking?
// TODO(peering): mkeeler asked that these maps come from proxycfg instead of // TODO(peering): mkeeler asked that these maps come from proxycfg instead of
@ -546,7 +606,10 @@ func makeRBACRules(
} }
// First build up just the basic principal matches. // First build up just the basic principal matches.
rbacIxns := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer) rbacIxns, err := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer, providerMap)
if err != nil {
return nil, err
}
// Normalize: if we are in default-deny then all intentions must be allows and vice versa // Normalize: if we are in default-deny then all intentions must be allows and vice versa
intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow) intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow)
@ -574,10 +637,6 @@ func makeRBACRules(
var principalsL4 []*envoy_rbac_v3.Principal var principalsL4 []*envoy_rbac_v3.Principal
for i, rbacIxn := range rbacIxns { for i, rbacIxn := range rbacIxns {
var infos []*JWTInfo
if isHTTP {
infos = collectJWTInfos(rbacIxn)
}
if rbacIxn.Action == intentionActionLayer7 { if rbacIxn.Action == intentionActionLayer7 {
if len(rbacIxn.Permissions) == 0 { if len(rbacIxn.Permissions) == 0 {
panic("invalid state: L7 intention has no permissions") panic("invalid state: L7 intention has no permissions")
@ -587,10 +646,6 @@ func makeRBACRules(
} }
rbacPrincipals := optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal}) 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 // For L7: we should generate one Policy per Principal and list all of the Permissions
policy := &envoy_rbac_v3.Policy{ policy := &envoy_rbac_v3.Policy{
Principals: rbacPrincipals, Principals: rbacPrincipals,
@ -603,11 +658,6 @@ func makeRBACRules(
} else { } else {
// For L4: we should generate one big Policy listing all Principals // For L4: we should generate one big Policy listing all Principals
principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal) 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 { if len(principalsL4) > 0 {
@ -620,59 +670,74 @@ func makeRBACRules(
if len(rbac.Policies) == 0 { if len(rbac.Policies) == 0 {
rbac.Policies = nil rbac.Policies = nil
} }
return rbac return rbac, nil
} }
// combineBasePrincipalWithJWTPrincipals ensure each RBAC/Network principal is associated with // addJWTPrincipal ensure the passed RBAC/Network principal is associated with
// the JWT principal // a JWT principal when JWTs validation is required.
func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, cp *envoy_rbac_v3.Principal) []*envoy_rbac_v3.Principal { //
res := make([]*envoy_rbac_v3.Principal, 0) // For each jwtInfo, this builds a first principal that validates that the jwt has the right issuer (`iss`).
// It collects all the claims principal and combines them into a single principal using jwtClaimsToPrincipals.
// It then combines the issuer principal and the claims principal into a single principal.
//
// After generating a single principal per info, it combines all the info principals into a single jwt OrPrincipal.
// This orPrincipal is then attached to the RBAC/NETWORK principal for jwt payload validation.
func addJWTPrincipal(principal *envoy_rbac_v3.Principal, infos []*JWTInfo) *envoy_rbac_v3.Principal {
if len(infos) == 0 {
return principal
}
jwtPrincipals := make([]*envoy_rbac_v3.Principal, 0, len(infos))
for _, info := range infos {
payloadKey := buildPayloadInMetadataKey(info.Provider.Name)
for _, principal := range p { // build jwt provider issuer principal
if principal != nil && cp != nil { segments := pathToSegments([]string{"iss"}, payloadKey)
p := andPrincipals([]*envoy_rbac_v3.Principal{principal, cp}) p := segmentToPrincipal(segments, info.Issuer)
res = append(res, p)
// add jwt provider claims principal if any
if cp := jwtClaimsToPrincipals(info.Provider.VerifyClaims, payloadKey); cp != nil {
p = andPrincipals([]*envoy_rbac_v3.Principal{p, cp})
} }
} jwtPrincipals = append(jwtPrincipals, p)
return res
} }
// collectJWTInfos extracts all the collected JWTInfos top level infos // make jwt principals into 1 single principal
// and permission level infos and returns them as a single array jwtFinalPrincipal := orPrincipals(jwtPrincipals)
func collectJWTInfos(rbacIxn *rbacIntention) []*JWTInfo {
infos := make([]*JWTInfo, 0, len(rbacIxn.jwtInfos))
if len(rbacIxn.jwtInfos) > 0 { if principal == nil {
infos = append(infos, rbacIxn.jwtInfos...) return jwtFinalPrincipal
}
for _, perm := range rbacIxn.Permissions {
infos = append(infos, perm.jwtInfos...)
} }
return infos return andPrincipals([]*envoy_rbac_v3.Principal{principal, jwtFinalPrincipal})
} }
func jwtInfosToPrincipals(c []*JWTInfo) *envoy_rbac_v3.Principal { func jwtClaimsToPrincipals(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Principal {
ps := make([]*envoy_rbac_v3.Principal, 0) ps := make([]*envoy_rbac_v3.Principal, 0)
for _, jwtInfo := range c { for _, claim := range claims {
if jwtInfo != nil { ps = append(ps, jwtClaimToPrincipal(claim, payloadkey))
for _, claim := range jwtInfo.Claims {
ps = append(ps, jwtClaimToPrincipal(claim, jwtInfo.MetadataPayloadKey))
} }
switch len(ps) {
case 0:
return nil
case 1:
return ps[0]
default:
return andPrincipals(ps)
} }
} }
return orPrincipals(ps)
}
// jwtClaimToPrincipal takes in a payloadkey which is the metadata key. This key is generated by using provider name, // 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 // and 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 // 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. // come from the Path included in the IntentionJWTClaimVerification param.
func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Principal { func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Principal {
segments := pathToSegments(c.Path, payloadKey) segments := pathToSegments(c.Path, payloadKey)
return segmentToPrincipal(segments, c.Value)
}
func segmentToPrincipal(segments []*envoy_matcher_v3.MetadataMatcher_PathSegment, v string) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{ return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_Metadata{ Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{ Metadata: &envoy_matcher_v3.MetadataMatcher{
@ -682,7 +747,41 @@ func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey st
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{ StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: c.Value, Exact: v,
},
},
},
},
},
},
}
}
func jwtInfosToPermission(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Permission {
ps := make([]*envoy_rbac_v3.Permission, 0, len(claims))
for _, claim := range claims {
ps = append(ps, jwtClaimToPermission(claim, payloadkey))
}
return andPermissions(ps)
}
func jwtClaimToPermission(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Permission {
segments := pathToSegments(c.Path, payloadKey)
return segmentToPermission(segments, c.Value)
}
func segmentToPermission(segments []*envoy_matcher_v3.MetadataMatcher_PathSegment, v string) *envoy_rbac_v3.Permission {
return &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_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: v,
}, },
}, },
}, },
@ -837,6 +936,10 @@ func countWild(src rbacService) int {
} }
func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal { func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
switch len(ids) {
case 1:
return ids[0]
default:
return &envoy_rbac_v3.Principal{ return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_AndIds{ Identifier: &envoy_rbac_v3.Principal_AndIds{
AndIds: &envoy_rbac_v3.Principal_Set{ AndIds: &envoy_rbac_v3.Principal_Set{
@ -845,8 +948,13 @@ func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
}, },
} }
} }
}
func orPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal { func orPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
switch len(ids) {
case 1:
return ids[0]
default:
return &envoy_rbac_v3.Principal{ return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_OrIds{ Identifier: &envoy_rbac_v3.Principal_OrIds{
OrIds: &envoy_rbac_v3.Principal_Set{ OrIds: &envoy_rbac_v3.Principal_Set{
@ -855,6 +963,7 @@ func orPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
}, },
} }
} }
}
func notPrincipal(id *envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal { func notPrincipal(id *envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{ return &envoy_rbac_v3.Principal{
@ -1206,3 +1315,20 @@ func andPermissions(perms []*envoy_rbac_v3.Permission) *envoy_rbac_v3.Permission
} }
} }
} }
func orPermissions(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_OrRules{
OrRules: &envoy_rbac_v3.Permission_Set{
Rules: perms,
},
},
}
}
}

View File

@ -451,10 +451,11 @@ func TestRemoveIntentionPrecedence(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
rbacIxns := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle) rbacIxns, err := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle, nil)
intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow) intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow)
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo) rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo)
require.NoError(t, err)
require.Equal(t, tt.expect, rbacIxns) require.Equal(t, tt.expect, rbacIxns)
}) })
} }
@ -529,6 +530,10 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
{Path: []string{"perms", "role"}, Value: "admin"}, {Path: []string{"perms", "role"}, Value: "admin"},
}, },
} }
testJWTProviderConfigEntry = map[string]*structs.JWTProviderConfigEntry{
"okta": {Name: "okta", Issuer: "mytest.okta-issuer"},
"auth0": {Name: "auth0", Issuer: "mytest.auth0-issuer"},
}
jwtRequirement = &structs.IntentionJWTRequirement{ jwtRequirement = &structs.IntentionJWTRequirement{
Providers: []*structs.IntentionJWTProvider{ Providers: []*structs.IntentionJWTProvider{
&oktaWithClaims, &oktaWithClaims,
@ -922,7 +927,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
}) })
}) })
t.Run("http filter", func(t *testing.T) { t.Run("http filter", func(t *testing.T) {
filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle) filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle, testJWTProviderConfigEntry)
require.NoError(t, err) require.NoError(t, err)
t.Run("current", func(t *testing.T) { t.Run("current", func(t *testing.T) {
@ -1202,7 +1207,7 @@ func TestPathToSegments(t *testing.T) {
} }
} }
func TestJwtClaimToPrincipal(t *testing.T) { func TestJWTClaimsToPrincipals(t *testing.T) {
var ( var (
firstClaim = structs.IntentionJWTClaimVerification{ firstClaim = structs.IntentionJWTClaimVerification{
Path: []string{"perms"}, Path: []string{"perms"},
@ -1234,7 +1239,7 @@ func TestJwtClaimToPrincipal(t *testing.T) {
Identifier: &envoy_rbac_v3.Principal_Metadata{ Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{ Metadata: &envoy_matcher_v3.MetadataMatcher{
Filter: jwtEnvoyFilter, Filter: jwtEnvoyFilter,
Path: pathToSegments(secondClaim.Path, "second-key"), Path: pathToSegments(secondClaim.Path, payloadKey),
Value: &envoy_matcher_v3.ValueMatcher{ Value: &envoy_matcher_v3.ValueMatcher{
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{ StringMatch: &envoy_matcher_v3.StringMatcher{
@ -1249,38 +1254,21 @@ func TestJwtClaimToPrincipal(t *testing.T) {
} }
) )
tests := map[string]struct { tests := map[string]struct {
jwtInfos []*JWTInfo claims []*structs.IntentionJWTClaimVerification
metadataPayloadKey string
expected *envoy_rbac_v3.Principal expected *envoy_rbac_v3.Principal
}{ }{
"single-jwt-info": { "single-claim": {
jwtInfos: []*JWTInfo{ claims: []*structs.IntentionJWTClaimVerification{&firstClaim},
{ metadataPayloadKey: payloadKey,
Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, expected: &firstPrincipal,
MetadataPayloadKey: payloadKey,
},
}, },
"multiple-claims": {
claims: []*structs.IntentionJWTClaimVerification{&firstClaim, &secondClaim},
metadataPayloadKey: payloadKey,
expected: &envoy_rbac_v3.Principal{ expected: &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_OrIds{ Identifier: &envoy_rbac_v3.Principal_AndIds{
OrIds: &envoy_rbac_v3.Principal_Set{ AndIds: &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}, Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal},
}, },
}, },
@ -1291,7 +1279,7 @@ func TestJwtClaimToPrincipal(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
principal := jwtInfosToPrincipals(tt.jwtInfos) principal := jwtClaimsToPrincipals(tt.claims, tt.metadataPayloadKey)
require.Equal(t, principal, tt.expected) require.Equal(t, principal, tt.expected)
}) })
} }

View File

@ -3,9 +3,9 @@
"typedConfig": { "typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication", "@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication",
"providers": { "providers": {
"okta_0": { "okta": {
"issuer": "test-issuer", "issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0", "payloadInMetadata": "jwt_payload_okta",
"remoteJwks": { "remoteJwks": {
"httpUri": { "httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json", "uri": "https://example-okta.com/.well-known/jwks.json",
@ -21,10 +21,15 @@
"rules": [ "rules": [
{ {
"match": { "match": {
"prefix": "some-special-path" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta_0" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -17,7 +17,12 @@
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -17,20 +17,6 @@
} }
} }
}, },
"okta_0": {
"issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0",
"remoteJwks": {
"httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json",
"cluster": "jwks_cluster_okta",
"timeout": "1s"
},
"asyncFetch": {
"fastListener": true
}
}
},
"auth0": { "auth0": {
"issuer": "another-issuer", "issuer": "another-issuer",
"payloadInMetadata": "jwt_payload_auth0", "payloadInMetadata": "jwt_payload_auth0",
@ -47,28 +33,32 @@
} }
}, },
"rules": [ "rules": [
{
"match": {
"prefix": "some-special-path"
},
"requires": {
"providerName": "okta_0"
}
},
{ {
"match": { "match": {
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAll": {
"requirements": [
{
"requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
} }
}, },
{ {
"match": { "requiresAny": {
"prefix": "/" "requirements": [
}, {"providerName": "auth0"},
"requires": { {"allowMissingOrFailed": {}}
"providerName": "auth0" ]
}
}
]
}
} }
} }
] ]

View File

@ -0,0 +1 @@
{}

View File

@ -24,7 +24,12 @@
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -16,37 +16,21 @@
"fastListener": true "fastListener": true
} }
} }
},
"okta_0": {
"issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0",
"remoteJwks": {
"httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json",
"cluster": "jwks_cluster_okta",
"timeout": "1s"
},
"asyncFetch": {
"fastListener": true
}
}
} }
}, },
"rules": [ "rules": [
{
"match": {
"prefix": "some-special-path"
},
"requires": {
"providerName": "okta_0"
}
},
{ {
"match": { "match": {
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -6,36 +6,38 @@
"policies": { "policies": {
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{
"andRules": {
"rules": [
{ {
"urlPath": { "urlPath": {
"path": { "path": {
"prefix": "some-path" "prefix": "some-path"
} }
} }
} },
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
], ],
"principals": [ "value": {
{ "stringMatch": {
"andIds": { "exact": "mytest.okta-issuer"
"ids": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"googleRe2": {},
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
} }
} }
} }
}, },
{
"orIds": {
"ids": [
{ {
"metadata": { "metadata": {
"filter": "envoy.filters.http.jwt_authn", "filter": "envoy.filters.http.jwt_authn",
"path": [ "path": [
{"key": "jwt_payload_okta_0"}, {"key": "jwt_payload_okta"},
{"key": "roles"} {"key": "roles"}
], ],
"value": { "value": {
@ -51,6 +53,18 @@
] ]
} }
} }
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"googleRe2": {},
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
] ]
} }
} }

View File

@ -27,8 +27,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",

View File

@ -6,6 +6,9 @@
"policies": { "policies": {
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{
"andRules": {
"rules": [
{ {
"urlPath": { "urlPath": {
"path": { "path": {
@ -13,6 +16,47 @@
} }
} }
}, },
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
}
]
}
},
{
"andRules": {
"rules": [
{ {
"andRules": { "andRules": {
"rules": [ "rules": [
@ -34,6 +78,44 @@
} }
] ]
} }
},
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
}
]
}
} }
], ],
"principals": [ "principals": [
@ -53,8 +135,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",
@ -68,36 +164,6 @@
} }
} }
} }
},
{
"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"
}
}
}
} }
] ]
} }

View File

@ -6,6 +6,9 @@
"policies": { "policies": {
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{
"andRules": {
"rules": [
{ {
"urlPath": { "urlPath": {
"path": { "path": {
@ -13,6 +16,44 @@
} }
} }
}, },
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
}
]
}
},
{ {
"andRules": { "andRules": {
"rules": [ "rules": [
@ -53,8 +94,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",
@ -68,21 +123,6 @@
} }
} }
} }
},
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0_0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
} }
] ]
} }

View File

@ -76,8 +76,8 @@ func TestJWTAuthConnectService(t *testing.T) {
configureIntentions(t, cluster) configureIntentions(t, cluster)
baseURL := fmt.Sprintf("http://localhost:%d", clientPort) baseURL := fmt.Sprintf("http://localhost:%d", clientPort)
// fails without jwt headers // TODO(roncodingenthusiast): update test to reflect jwt-auth filter in metadata mode
doRequest(t, baseURL, http.StatusUnauthorized, "") doRequest(t, baseURL, http.StatusOK, "")
// succeeds with jwt // succeeds with jwt
doRequest(t, baseURL, http.StatusOK, jwt) doRequest(t, baseURL, http.StatusOK, jwt)
} }