From a87bae636b3aab402c3a9da51e7bab8a3a54c612 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:22:48 +0000 Subject: [PATCH 01/13] generate auth_jwt_claim_set directive Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- internal/configs/version2/http.go | 2 +- .../version2/nginx-plus.virtualserver.tmpl | 2 +- internal/configs/version2/templates_test.go | 2 +- internal/configs/virtualserver.go | 46 +++++++++---- internal/configs/virtualserver_test.go | 68 +++++++++++++++++++ pkg/apis/configuration/v1/types.go | 32 ++++++--- 6 files changed, 127 insertions(+), 25 deletions(-) diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index 152706fba6..7d5e7f7e3e 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -32,7 +32,7 @@ type VirtualServerConfig struct { // AuthJwtClaimSet defines the values for the `auth_jwt_claim_set` directive type AuthJwtClaimSet struct { Variable string - Claims string + Claim string } // Upstream defines an upstream. diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 4ac0967984..41c36b6249 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -51,7 +51,7 @@ split_clients {{ $sc.Source }} {{ $sc.Variable }} { {{- end }} {{- range $claim := .AuthJwtClaimSet }} -auth_jwt_claim_set {{ $claim.Variable }} {{ $claim.Claims}} +auth_jwt_claim_set {{ $claim.Variable }} {{ $claim.Claim}} {{- end }} {{- range $m := .Maps }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 7dc0739a58..dc18064d8e 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -1577,7 +1577,7 @@ var ( AuthJwtClaimSet: []AuthJwtClaimSet{ { Variable: "$jwt_default_webapp_group_consumer_group_type", - Claims: "consumer_group type", + Claim: "consumer_group type", }, }, Maps: []Map{ diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 8535467b87..44f6e81f78 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -914,18 +914,19 @@ type apiKeyAuth struct { } type policiesCfg struct { - Allow []string - Deny []string - RateLimit rateLimit - JWTAuth jwtAuth - BasicAuth *version2.BasicAuth - IngressMTLS *version2.IngressMTLS - EgressMTLS *version2.EgressMTLS - OIDC bool - APIKey apiKeyAuth - WAF *version2.WAF - ErrorReturn *version2.Return - BundleValidator bundleValidator + Allow []string + Deny []string + RateLimit rateLimit + JWTAuth jwtAuth + AuthJwtClaimSets []*version2.AuthJwtClaimSet + BasicAuth *version2.BasicAuth + IngressMTLS *version2.IngressMTLS + EgressMTLS *version2.EgressMTLS + OIDC bool + APIKey apiKeyAuth + WAF *version2.WAF + ErrorReturn *version2.Return + BundleValidator bundleValidator } type bundleValidator interface { @@ -1011,6 +1012,10 @@ func (p *policiesCfg) addRateLimitConfig( rlZoneName := fmt.Sprintf("pol_rl_%v_%v_%v_%v", polNamespace, polName, vsNamespace, vsName) p.RateLimit.Reqs = append(p.RateLimit.Reqs, generateLimitReq(rlZoneName, rateLimit)) p.RateLimit.Zones = append(p.RateLimit.Zones, generateLimitReqZone(rlZoneName, rateLimit, podReplicas)) + if rateLimit.Condition != nil && rateLimit.Condition.JWT != nil { + p.AuthJwtClaimSets = append(p.AuthJwtClaimSets, generateAuthJwtClaimSet(*rateLimit.Condition.JWT, vsNamespace, vsName)) + } + //p.AuthJwtClaimSet = app if len(p.RateLimit.Reqs) == 1 { p.RateLimit.Options = generateLimitReqOptions(rateLimit) } else { @@ -1667,6 +1672,23 @@ func removeDuplicateLimitReqZones(rlz []version2.LimitReqZone) []version2.LimitR return result } +func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace string, vsName string) *version2.AuthJwtClaimSet { + return &version2.AuthJwtClaimSet{ + Variable: generateAuthJwtClaimSetVariable(jwtCondition.Claim, vsNamespace, vsName), + Claim: generateAuthJwtClaimSetClaim(jwtCondition.Claim), + } +} + +// TODO: process claim with spaces +func generateAuthJwtClaimSetVariable(claim string, vsNamespace string, vsName string) string { + return fmt.Sprintf("jwt_%v_%v_%v", vsNamespace, vsName, strings.Join(strings.Split(claim, "."), "_")) +} + +// TODO: process claim with spaces +func generateAuthJwtClaimSetClaim(claim string) string { + return strings.Join(strings.Split(claim, "."), " ") +} + func addPoliciesCfgToLocation(cfg policiesCfg, location *version2.Location) { location.Allow = cfg.Allow location.Deny = cfg.Deny diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 93af610053..058572ea30 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -9322,6 +9322,74 @@ func TestGenerateString(t *testing.T) { } } +func TestGenerateAuthJwtClaimSetVariable(t *testing.T) { + t.Parallel() + tests := []struct { + claim string + vsNamespace string + vsName string + expected string + }{ + { + claim: "consumer_group.type", + vsNamespace: "default", + vsName: "webapp", + expected: "jwt_default_webapp_consumer_group_type", + }, + { + claim: "type", + vsNamespace: "default", + vsName: "webapp", + expected: "jwt_default_webapp_type", + }, + { + claim: "a.b.c", + vsNamespace: "default", + vsName: "webapp", + expected: "jwt_default_webapp_a_b_c", + }, + } + + for _, test := range tests { + result := generateAuthJwtClaimSetVariable(test.claim, test.vsNamespace, test.vsName) + if result != test.expected { + t.Errorf("generateAuthJwtClaimSetVariable() return %v but expected %v", result, test.expected) + } + } +} + +func TestGenerateAuthJwtClaimSetClaim(t *testing.T) { + t.Parallel() + tests := []struct { + claim string + expected string + }{ + { + claim: "consumer_group.type", + expected: "consumer_group type", + }, + { + claim: "consumer_group.type", + expected: "consumer_group type", + }, + { + claim: "type", + expected: "type", + }, + { + claim: "a.b.c", + expected: "a b c", + }, + } + + for _, test := range tests { + result := generateAuthJwtClaimSetClaim(test.claim) + if result != test.expected { + t.Errorf("generateAuthJwtClaimSetClaim() return %v but expected %v", result, test.expected) + } + } +} + func TestGenerateSnippets(t *testing.T) { t.Parallel() tests := []struct { diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index cac87569ab..9829dd7407 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -600,16 +600,28 @@ type AccessControl struct { // RateLimit defines a rate limit policy. type RateLimit struct { - Rate string `json:"rate"` - Key string `json:"key"` - Delay *int `json:"delay"` - NoDelay *bool `json:"noDelay"` - Burst *int `json:"burst"` - ZoneSize string `json:"zoneSize"` - DryRun *bool `json:"dryRun"` - LogLevel string `json:"logLevel"` - RejectCode *int `json:"rejectCode"` - Scale bool `json:"scale"` + Rate string `json:"rate"` + Key string `json:"key"` + Delay *int `json:"delay"` + NoDelay *bool `json:"noDelay"` + Burst *int `json:"burst"` + ZoneSize string `json:"zoneSize"` + DryRun *bool `json:"dryRun"` + LogLevel string `json:"logLevel"` + RejectCode *int `json:"rejectCode"` + Scale bool `json:"scale"` + Condition *RateLimitCondition `json:"condition"` +} + +// TODO: add valition at CRD level +type RateLimitCondition struct { + JWT *JWTCondition `json:"jwt"` + Default bool `json:"default"` +} + +type JWTCondition struct { + Claim string `json:"claim"` + Match string `json:"match"` } // JWTAuth holds JWT authentication configuration. From 029e02ea2e16a743c7539b547f11cfdbf3a62e25 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:43:53 +0000 Subject: [PATCH 02/13] add auth_jwt_claim_set to spec, route, subroute Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- internal/configs/version2/http.go | 6 +-- .../version2/nginx-plus.virtualserver.tmpl | 2 +- internal/configs/version2/templates_test.go | 2 +- internal/configs/virtualserver.go | 43 ++++++++++++++----- internal/configs/virtualserver_test.go | 1 + 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index 7d5e7f7e3e..4b6a80b13f 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -18,7 +18,7 @@ type VirtualServerConfig struct { KeyVals []KeyVal LimitReqZones []LimitReqZone Maps []Map - AuthJwtClaimSet []AuthJwtClaimSet + AuthJWTClaimSets []AuthJWTClaimSet Server Server SpiffeCerts bool SpiffeClientCerts bool @@ -29,8 +29,8 @@ type VirtualServerConfig struct { StaticSSLPath string } -// AuthJwtClaimSet defines the values for the `auth_jwt_claim_set` directive -type AuthJwtClaimSet struct { +// AuthJWTClaimSet defines the values for the `auth_jwt_claim_set` directive +type AuthJWTClaimSet struct { Variable string Claim string } diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 41c36b6249..a57e96ffdf 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -50,7 +50,7 @@ split_clients {{ $sc.Source }} {{ $sc.Variable }} { } {{- end }} -{{- range $claim := .AuthJwtClaimSet }} +{{- range $claim := .AuthJWTClaimSets }} auth_jwt_claim_set {{ $claim.Variable }} {{ $claim.Claim}} {{- end }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index dc18064d8e..5aa546fc76 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -1574,7 +1574,7 @@ var ( }, }, Upstreams: []Upstream{}, - AuthJwtClaimSet: []AuthJwtClaimSet{ + AuthJWTClaimSets: []AuthJWTClaimSet{ { Variable: "$jwt_default_webapp_group_consumer_group_type", Claim: "consumer_group type", diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 44f6e81f78..db83227d70 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -453,9 +453,12 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( var statusMatches []version2.StatusMatch var healthChecks []version2.HealthCheck var limitReqZones []version2.LimitReqZone + var authJWTClaimSets []*version2.AuthJWTClaimSet limitReqZones = append(limitReqZones, policiesCfg.RateLimit.Zones...) + authJWTClaimSets = append(authJWTClaimSets, policiesCfg.AuthJWTClaimSets...) + // generate upstreams for VirtualServer for _, u := range vsEx.VirtualServer.Spec.Upstreams { @@ -606,6 +609,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( } limitReqZones = append(limitReqZones, routePoliciesCfg.RateLimit.Zones...) + authJWTClaimSets = append(authJWTClaimSets, routePoliciesCfg.AuthJWTClaimSets...) + dosRouteCfg := generateDosCfg(dosResources[r.Path]) if len(r.Matches) > 0 { @@ -747,6 +752,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( limitReqZones = append(limitReqZones, routePoliciesCfg.RateLimit.Zones...) + authJWTClaimSets = append(authJWTClaimSets, routePoliciesCfg.AuthJWTClaimSets...) + dosRouteCfg := generateDosCfg(dosResources[r.Path]) if len(r.Matches) > 0 { @@ -828,12 +835,13 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( }) vsCfg := version2.VirtualServerConfig{ - Upstreams: upstreams, - SplitClients: splitClients, - Maps: maps, - StatusMatches: statusMatches, - LimitReqZones: removeDuplicateLimitReqZones(limitReqZones), - HTTPSnippets: httpSnippets, + Upstreams: upstreams, + SplitClients: splitClients, + Maps: maps, + StatusMatches: statusMatches, + LimitReqZones: removeDuplicateLimitReqZones(limitReqZones), + AuthJWTClaimSets: removeDuplicateAuthJWTClaimSets(authJWTClaimSets), + HTTPSnippets: httpSnippets, Server: version2.Server{ ServerName: vsEx.VirtualServer.Spec.Host, Gunzip: vsEx.VirtualServer.Spec.Gunzip, @@ -918,7 +926,7 @@ type policiesCfg struct { Deny []string RateLimit rateLimit JWTAuth jwtAuth - AuthJwtClaimSets []*version2.AuthJwtClaimSet + AuthJWTClaimSets []*version2.AuthJWTClaimSet BasicAuth *version2.BasicAuth IngressMTLS *version2.IngressMTLS EgressMTLS *version2.EgressMTLS @@ -1013,9 +1021,8 @@ func (p *policiesCfg) addRateLimitConfig( p.RateLimit.Reqs = append(p.RateLimit.Reqs, generateLimitReq(rlZoneName, rateLimit)) p.RateLimit.Zones = append(p.RateLimit.Zones, generateLimitReqZone(rlZoneName, rateLimit, podReplicas)) if rateLimit.Condition != nil && rateLimit.Condition.JWT != nil { - p.AuthJwtClaimSets = append(p.AuthJwtClaimSets, generateAuthJwtClaimSet(*rateLimit.Condition.JWT, vsNamespace, vsName)) + p.AuthJWTClaimSets = append(p.AuthJWTClaimSets, generateAuthJwtClaimSet(*rateLimit.Condition.JWT, vsNamespace, vsName)) } - //p.AuthJwtClaimSet = app if len(p.RateLimit.Reqs) == 1 { p.RateLimit.Options = generateLimitReqOptions(rateLimit) } else { @@ -1672,8 +1679,22 @@ func removeDuplicateLimitReqZones(rlz []version2.LimitReqZone) []version2.LimitR return result } -func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace string, vsName string) *version2.AuthJwtClaimSet { - return &version2.AuthJwtClaimSet{ +func removeDuplicateAuthJWTClaimSets(ajcs []*version2.AuthJWTClaimSet) []version2.AuthJWTClaimSet { + encountered := make(map[string]bool) + var result []version2.AuthJWTClaimSet + + for _, v := range ajcs { + if !encountered[v.Variable] { + encountered[v.Variable] = true + result = append(result, *v) + } + } + + return result +} + +func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace string, vsName string) *version2.AuthJWTClaimSet { + return &version2.AuthJWTClaimSet{ Variable: generateAuthJwtClaimSetVariable(jwtCondition.Claim, vsNamespace, vsName), Claim: generateAuthJwtClaimSetClaim(jwtCondition.Claim), } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 058572ea30..53e1fbb110 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -8919,6 +8919,7 @@ func TestRemoveDuplicates(t *testing.T) { } } +// TODO: write test for removeDuplicateAuthJWTClaimSets func TestAddPoliciesCfgToLocations(t *testing.T) { t.Parallel() cfg := policiesCfg{ From 81e6368ad83564e86df26421361bb3dca204cf43 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:54:33 +0000 Subject: [PATCH 03/13] add test, fix template, remove pointers, generate crd Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- config/crd/bases/k8s.nginx.org_policies.yaml | 12 + deploy/crds.yaml | 12 + .../__snapshots__/templates_test.snap | 2 +- .../version2/nginx-plus.virtualserver.tmpl | 2 +- internal/configs/virtualserver.go | 20 +- internal/configs/virtualserver_test.go | 301 +++++++++++++++++- pkg/apis/configuration/v1/types.go | 4 +- 7 files changed, 335 insertions(+), 18 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index 7bf119c71b..2bb1348bbf 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -179,6 +179,18 @@ spec: properties: burst: type: integer + condition: + properties: + default: + type: boolean + jwt: + properties: + claim: + type: string + match: + type: string + type: object + type: object delay: type: integer dryRun: diff --git a/deploy/crds.yaml b/deploy/crds.yaml index c6601ee07f..91f87821f5 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -341,6 +341,18 @@ spec: properties: burst: type: integer + condition: + properties: + default: + type: boolean + jwt: + properties: + claim: + type: string + match: + type: string + type: object + type: object delay: type: integer dryRun: diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index e3aef875f5..2dfe09753b 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -2263,7 +2263,7 @@ server { [TestExecuteVirtualServerTemplate_RendersTemplateWithRateLimitJWTClaim - 1] -auth_jwt_claim_set $jwt_default_webapp_group_consumer_group_type consumer_group type +auth_jwt_claim_set $jwt_default_webapp_group_consumer_group_type consumer_group type; map $jwt_default_webapp_group_consumer_group_type $rate_limit_default_webapp_group_consumer_group_type { default Group3; Gold Group1; diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index a57e96ffdf..49bd62712f 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -51,7 +51,7 @@ split_clients {{ $sc.Source }} {{ $sc.Variable }} { {{- end }} {{- range $claim := .AuthJWTClaimSets }} -auth_jwt_claim_set {{ $claim.Variable }} {{ $claim.Claim}} +auth_jwt_claim_set {{ $claim.Variable }} {{ $claim.Claim}}; {{- end }} {{- range $m := .Maps }} diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index db83227d70..52344a3084 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -453,7 +453,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( var statusMatches []version2.StatusMatch var healthChecks []version2.HealthCheck var limitReqZones []version2.LimitReqZone - var authJWTClaimSets []*version2.AuthJWTClaimSet + var authJWTClaimSets []version2.AuthJWTClaimSet limitReqZones = append(limitReqZones, policiesCfg.RateLimit.Zones...) @@ -695,7 +695,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( } locSnippets := r.LocationSnippets - // use the VirtualServer location snippet if the route does not define any + // use the VirtualServer location snippet if the route does not define any if r.LocationSnippets == "" { locSnippets = vsrLocationSnippetsFromVs[vsrNamespaceName] } @@ -926,7 +926,7 @@ type policiesCfg struct { Deny []string RateLimit rateLimit JWTAuth jwtAuth - AuthJWTClaimSets []*version2.AuthJWTClaimSet + AuthJWTClaimSets []version2.AuthJWTClaimSet BasicAuth *version2.BasicAuth IngressMTLS *version2.IngressMTLS EgressMTLS *version2.EgressMTLS @@ -1020,8 +1020,8 @@ func (p *policiesCfg) addRateLimitConfig( rlZoneName := fmt.Sprintf("pol_rl_%v_%v_%v_%v", polNamespace, polName, vsNamespace, vsName) p.RateLimit.Reqs = append(p.RateLimit.Reqs, generateLimitReq(rlZoneName, rateLimit)) p.RateLimit.Zones = append(p.RateLimit.Zones, generateLimitReqZone(rlZoneName, rateLimit, podReplicas)) - if rateLimit.Condition != nil && rateLimit.Condition.JWT != nil { - p.AuthJWTClaimSets = append(p.AuthJWTClaimSets, generateAuthJwtClaimSet(*rateLimit.Condition.JWT, vsNamespace, vsName)) + if rateLimit.Condition != nil && rateLimit.Condition.JWT.Claim != "" && rateLimit.Condition.JWT.Match != "" { + p.AuthJWTClaimSets = append(p.AuthJWTClaimSets, generateAuthJwtClaimSet(rateLimit.Condition.JWT, vsNamespace, vsName)) } if len(p.RateLimit.Reqs) == 1 { p.RateLimit.Options = generateLimitReqOptions(rateLimit) @@ -1679,22 +1679,22 @@ func removeDuplicateLimitReqZones(rlz []version2.LimitReqZone) []version2.LimitR return result } -func removeDuplicateAuthJWTClaimSets(ajcs []*version2.AuthJWTClaimSet) []version2.AuthJWTClaimSet { +func removeDuplicateAuthJWTClaimSets(ajcs []version2.AuthJWTClaimSet) []version2.AuthJWTClaimSet { encountered := make(map[string]bool) var result []version2.AuthJWTClaimSet for _, v := range ajcs { if !encountered[v.Variable] { encountered[v.Variable] = true - result = append(result, *v) + result = append(result, v) } } return result } -func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace string, vsName string) *version2.AuthJWTClaimSet { - return &version2.AuthJWTClaimSet{ +func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace string, vsName string) version2.AuthJWTClaimSet { + return version2.AuthJWTClaimSet{ Variable: generateAuthJwtClaimSetVariable(jwtCondition.Claim, vsNamespace, vsName), Claim: generateAuthJwtClaimSetClaim(jwtCondition.Claim), } @@ -1702,7 +1702,7 @@ func generateAuthJwtClaimSet(jwtCondition conf_v1.JWTCondition, vsNamespace stri // TODO: process claim with spaces func generateAuthJwtClaimSetVariable(claim string, vsNamespace string, vsName string) string { - return fmt.Sprintf("jwt_%v_%v_%v", vsNamespace, vsName, strings.Join(strings.Split(claim, "."), "_")) + return fmt.Sprintf("$jwt_%v_%v_%v", vsNamespace, vsName, strings.Join(strings.Split(claim, "."), "_")) } // TODO: process claim with spaces diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 53e1fbb110..2271f5e8c7 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -6395,6 +6395,233 @@ func TestGenerateVirtualServerConfigAPIKeyClientMaps(t *testing.T) { } } +func TestGenerateVirtualServerConfigRateLimitPolicyAuthJwt(t *testing.T) { + t.Parallel() + + dryRunOverride := true + rejectCodeOverride := 505 + + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Policies: []conf_v1.PolicyReference{ + { + Name: "gold-rate-limit-policy", + }, + { + Name: "silver-rate-limit-policy", + }, + }, + Upstreams: []conf_v1.Upstream{ + { + Name: "tea", + Service: "tea-svc", + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Action: &conf_v1.Action{ + Pass: "tea", + }, + }, + { + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + }, + }, + }, + Policies: map[string]*conf_v1.Policy{ + "default/gold-rate-limit-policy": { + Spec: conf_v1.PolicySpec{ + RateLimit: &conf_v1.RateLimit{ + Key: "test", + ZoneSize: "10M", + Rate: "10r/s", + DryRun: &dryRunOverride, + RejectCode: &rejectCodeOverride, + Condition: &conf_v1.RateLimitCondition{ + JWT: conf_v1.JWTCondition{ + Claim: "user_type.tier", + Match: "gold", + }, + }, + }, + }, + }, + "default/silver-rate-limit-policy": { + Spec: conf_v1.PolicySpec{ + RateLimit: &conf_v1.RateLimit{ + Key: "test", + ZoneSize: "20M", + Rate: "20r/s", + DryRun: &dryRunOverride, + //LogLevel: "info", + RejectCode: &rejectCodeOverride, + Condition: &conf_v1.RateLimitCondition{ + JWT: conf_v1.JWTCondition{ + Claim: "user_type.tier", + Match: "silver", + }, + }, + }, + }, + }, + }, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", + }, + "default/coffee-svc:80": { + "10.0.0.30:80", + }, + }, + } + + expected := version2.VirtualServerConfig{ + Maps: nil, + AuthJWTClaimSets: []version2.AuthJWTClaimSet{{Variable: "$jwt_default_cafe_user_type_tier", Claim: "user_type tier"}}, + + Upstreams: []version2.Upstream{ + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.30:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.20:80", + }, + }, + Keepalive: 16, + }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{ + {"test", "pol_rl_default_gold-rate-limit-policy_default_cafe", "10M", "10r/s"}, + {"test", "pol_rl_default_silver-rate-limit-policy_default_cafe", "20M", "20r/s"}, + }, + Server: version2.Server{ + JWTAuthList: nil, + JWTAuth: nil, + JWKSAuthEnabled: false, + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + ProxyProtocol: true, + ServerTokens: "off", + RealIPHeader: "X-Real-IP", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, + VSNamespace: "default", + VSName: "cafe", + APIKeyEnabled: false, + LimitReqs: []version2.LimitReq{ + {"pol_rl_default_gold-rate-limit-policy_default_cafe", 0, false, 0}, + {"pol_rl_default_silver-rate-limit-policy_default_cafe", 0, false, 0}, + }, + LimitReqOptions: version2.LimitReqOptions{ + DryRun: true, + LogLevel: "error", + RejectCode: 505, + }, + Locations: []version2.Location{ + { + Path: "/tea", + ProxyPass: "http://vs_default_cafe_tea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", + }, + { + Path: "/coffee", + ProxyPass: "http://vs_default_cafe_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + }, + }, + }, + } + + baseCfgParams := ConfigParams{ + Context: context.Background(), + ServerTokens: "off", + Keepalive: 16, + ServerSnippets: []string{"# server snippet"}, + ProxyProtocol: true, + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + } + + vsc := newVirtualServerConfigurator( + &baseCfgParams, + false, + false, + &StaticConfigParams{TLSPassthrough: true}, + false, + &fakeBV, + ) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + + sort.Slice(result.Maps, func(i, j int) bool { + return result.Maps[i].Variable < result.Maps[j].Variable + }) + + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + } +} + func TestGeneratePolicies(t *testing.T) { t.Parallel() ownerDetails := policyOwnerDetails{ @@ -8877,7 +9104,7 @@ func TestGeneratePoliciesFails(t *testing.T) { } } -func TestRemoveDuplicates(t *testing.T) { +func TestRemoveDuplicateLimitReqZones(t *testing.T) { t.Parallel() tests := []struct { rlz []version2.LimitReqZone @@ -8920,6 +9147,72 @@ func TestRemoveDuplicates(t *testing.T) { } // TODO: write test for removeDuplicateAuthJWTClaimSets +func TestRemoveDuplicateAuthJWTClaimSets(t *testing.T) { + t.Parallel() + tests := []struct { + ajcs []version2.AuthJWTClaimSet + expected []version2.AuthJWTClaimSet + }{ + { + ajcs: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + }, + expected: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + }, + }, + { + ajcs: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + }, + expected: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + }, + }, + { + ajcs: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + { + Variable: "$jwt_default_webapp_user_group_type", + }, + }, + expected: []version2.AuthJWTClaimSet{ + { + Variable: "$jwt_default_webapp_consumer_group_type", + }, + { + Variable: "$jwt_default_webapp_user_group_type", + }, + }, + }, + } + for _, test := range tests { + result := removeDuplicateAuthJWTClaimSets(test.ajcs) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("removeDuplicateAuthJWTClaimSets() returned \n%v, but expected \n%v", result, test.expected) + } + } +} + func TestAddPoliciesCfgToLocations(t *testing.T) { t.Parallel() cfg := policiesCfg{ @@ -9335,19 +9628,19 @@ func TestGenerateAuthJwtClaimSetVariable(t *testing.T) { claim: "consumer_group.type", vsNamespace: "default", vsName: "webapp", - expected: "jwt_default_webapp_consumer_group_type", + expected: "$jwt_default_webapp_consumer_group_type", }, { claim: "type", vsNamespace: "default", vsName: "webapp", - expected: "jwt_default_webapp_type", + expected: "$jwt_default_webapp_type", }, { claim: "a.b.c", vsNamespace: "default", vsName: "webapp", - expected: "jwt_default_webapp_a_b_c", + expected: "$jwt_default_webapp_a_b_c", }, } diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 9829dd7407..1e528734f4 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -615,8 +615,8 @@ type RateLimit struct { // TODO: add valition at CRD level type RateLimitCondition struct { - JWT *JWTCondition `json:"jwt"` - Default bool `json:"default"` + JWT JWTCondition `json:"jwt"` + Default bool `json:"default"` } type JWTCondition struct { From 8af81368236767e9852105ab4761115351443d86 Mon Sep 17 00:00:00 2001 From: Paul Abel