diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 4e5fca873a..6adbb716df 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -5,23 +5,31 @@ import ( "testing" ) -const ( - nginxPlusVirtualServerTmpl = "nginx-plus.virtualserver.tmpl" - nginxVirtualServerTmpl = "nginx.virtualserver.tmpl" - nginxPlusTransportServerTmpl = "nginx-plus.transportserver.tmpl" - nginxTransportServerTmpl = "nginx.transportserver.tmpl" -) - func createPointerFromInt(n int) *int { return &n } -func TestVirtualServerForNginxPlus(t *testing.T) { - t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) +func newTmplExecutorNGINXPlus(t *testing.T) *TemplateExecutor { + t.Helper() + executor, err := NewTemplateExecutor("nginx-plus.virtualserver.tmpl", "nginx-plus.transportserver.tmpl") + if err != nil { + t.Fatal(err) + } + return executor +} + +func newTmplExecutorNGINX(t *testing.T) *TemplateExecutor { + t.Helper() + executor, err := NewTemplateExecutor("nginx.virtualserver.tmpl", "nginx.transportserver.tmpl") if err != nil { - t.Fatalf("Failed to create template executor: %v", err) + t.Fatal(err) } + return executor +} + +func TestVirtualServerForNginxPlus(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) data, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfg) if err != nil { t.Errorf("Failed to execute template: %v", err) @@ -31,10 +39,7 @@ func TestVirtualServerForNginxPlus(t *testing.T) { func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOn(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } + executor := newTmplExecutorNGINXPlus(t) got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithGunzipOn) if err != nil { t.Error(err) @@ -47,10 +52,7 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOn(t *testi func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOff(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } + executor := newTmplExecutorNGINXPlus(t) got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithGunzipOff) if err != nil { t.Error(err) @@ -63,10 +65,7 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOff(t *test func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipNotSet(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } + executor := newTmplExecutorNGINXPlus(t) got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithGunzipNotSet) if err != nil { t.Error(err) @@ -79,10 +78,7 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipNotSet(t *t func TestExecuteVirtualServerTemplate_RendersTemplateWithSessionCookieSameSite(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } + executor := newTmplExecutorNGINXPlus(t) got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithSessionCookieSameSite) if err != nil { t.Error(err) @@ -95,10 +91,7 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithSessionCookieSameSite(t func TestVirtualServerForNginxPlusWithWAFApBundle(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatalf("Failed to create template executor: %v", err) - } + executor := newTmplExecutorNGINXPlus(t) data, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithWAFApBundle) if err != nil { t.Errorf("Failed to execute template: %v", err) @@ -108,11 +101,7 @@ func TestVirtualServerForNginxPlusWithWAFApBundle(t *testing.T) { func TestVirtualServerForNginx(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxVirtualServerTmpl, nginxTransportServerTmpl) - if err != nil { - t.Fatalf("Failed to create template executor: %v", err) - } - + executor := newTmplExecutorNGINX(t) data, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfg) if err != nil { t.Errorf("Failed to execute template: %v", err) @@ -122,11 +111,7 @@ func TestVirtualServerForNginx(t *testing.T) { func TestTransportServerForNginxPlus(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatalf("Failed to create template executor: %v", err) - } - + executor := newTmplExecutorNGINXPlus(t) data, err := executor.ExecuteTransportServerTemplate(&transportServerCfg) if err != nil { t.Errorf("Failed to execute template: %v", err) @@ -136,11 +121,8 @@ func TestTransportServerForNginxPlus(t *testing.T) { func TestExecuteTemplateForTransportServerWithResolver(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } - _, err = executor.ExecuteTransportServerTemplate(&transportServerCfgWithResolver) + executor := newTmplExecutorNGINXPlus(t) + _, err := executor.ExecuteTransportServerTemplate(&transportServerCfgWithResolver) if err != nil { t.Errorf("Failed to execute template: %v", err) } @@ -148,11 +130,7 @@ func TestExecuteTemplateForTransportServerWithResolver(t *testing.T) { func TestTransportServerForNginx(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxVirtualServerTmpl, nginxTransportServerTmpl) - if err != nil { - t.Fatalf("Failed to create template executor: %v", err) - } - + executor := newTmplExecutorNGINX(t) data, err := executor.ExecuteTransportServerTemplate(&transportServerCfg) if err != nil { t.Errorf("Failed to execute template: %v", err) @@ -162,10 +140,7 @@ func TestTransportServerForNginx(t *testing.T) { func TestTLSPassthroughHosts(t *testing.T) { t.Parallel() - executor, err := NewTemplateExecutor(nginxVirtualServerTmpl, nginxTransportServerTmpl) - if err != nil { - t.Fatalf("Failed to create template executor: %v", err) - } + executor := newTmplExecutorNGINX(t) unixSocketsCfg := TLSPassthroughHostsConfig{ "app.example.com": "unix:/var/lib/nginx/passthrough-default_secure-app.sock", @@ -178,6 +153,44 @@ func TestTLSPassthroughHosts(t *testing.T) { t.Log(string(data)) } +func TestExecuteVirtualServerTemplateWithJWKSWithToken(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTPolicyJWKSWithToken) + if err != nil { + t.Error(err) + } + if !bytes.Contains(got, []byte("token=$http_token")) { + t.Error("want `token=$http_token` in generated template") + } + if !bytes.Contains(got, []byte("proxy_cache jwks_uri_")) { + t.Error("want `proxy_cache` in generated template") + } + if !bytes.Contains(got, []byte("proxy_cache_valid 200 12h;")) { + t.Error("want `proxy_cache_valid 200 12h;` in generated template") + } + t.Log(string(got)) +} + +func TestExecuteVirtualServerTemplateWithJWKSWithoutToken(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTPolicyJWKSWithoutToken) + if err != nil { + t.Error(err) + } + if bytes.Contains(got, []byte("token=$http_token")) { + t.Error("want no `token=$http_token` string in generated template") + } + if !bytes.Contains(got, []byte("proxy_cache jwks_uri_")) { + t.Error("want `proxy_cache` in generated template") + } + if !bytes.Contains(got, []byte("proxy_cache_valid 200 12h;")) { + t.Error("want `proxy_cache_valid 200 12h;` in generated template") + } + t.Log(string(got)) +} + var ( virtualServerCfg = VirtualServerConfig{ LimitReqZones: []LimitReqZone{ @@ -2258,6 +2271,281 @@ var ( }, } + // VirtualServer Config data for JWT Policy tests + + virtualServerCfgWithJWTPolicyJWKSWithToken = VirtualServerConfig{ + Upstreams: []Upstream{ + { + UpstreamLabels: UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []UpstreamServer{ + { + Address: "10.0.0.20:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_coffee", + Servers: []UpstreamServer{ + { + Address: "10.0.0.30:80", + }, + }, + Keepalive: 16, + }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []LimitReqZone{}, + Server: Server{ + JWTAuthList: map[string]*JWTAuth{ + "default/jwt-policy": { + Key: "default/jwt-policy", + Realm: "Spec Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + }, + }, + "default/jwt-policy-route": { + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy", + Realm: "Spec Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + }, + }, + JWKSAuthEnabled: true, + 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", + Locations: []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: []Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + { + 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: []Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + }, + }, + } + + virtualServerCfgWithJWTPolicyJWKSWithoutToken = VirtualServerConfig{ + Upstreams: []Upstream{ + { + UpstreamLabels: UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []UpstreamServer{ + { + Address: "10.0.0.20:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_coffee", + Servers: []UpstreamServer{ + { + Address: "10.0.0.30:80", + }, + }, + Keepalive: 16, + }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []LimitReqZone{}, + Server: Server{ + JWTAuthList: map[string]*JWTAuth{ + "default/jwt-policy": { + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + }, + }, + "default/jwt-policy-route": { + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + }, + }, + JWKSAuthEnabled: true, + 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", + Locations: []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: []Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + { + 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: []Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + JWTAuth: &JWTAuth{ + Key: "default/jwt-policy-route", + Realm: "Route Realm API", + KeyCache: "1h", + JwksURI: JwksURI{ + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + }, + }, + }, + }, + }, + } + transportServerCfg = TransportServerConfig{ Upstreams: []StreamUpstream{ { diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index c855c6ce41..228300d24c 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -149,39 +149,55 @@ func validateRateLimit(rateLimit *v1.RateLimit, fieldPath *field.Path, isPlus bo return allErrs } +// validateJWT validates JWT Policy according the rules specified in documentation +// for using [jwt] local k8s secrets and using [jwks] from remote location. +// +// [jwt]: https://docs.nginx.com/nginx-ingress-controller/configuration/policy-resource/#jwt-using-local-kubernetes-secret +// [jwks]: https://docs.nginx.com/nginx-ingress-controller/configuration/policy-resource/#jwt-using-jwks-from-remote-location func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - + // Realm is always required. if jwt.Realm == "" { - allErrs = append(allErrs, field.Required(fieldPath, "")) - } else { - allErrs = append(allErrs, validateRealm(jwt.Realm, fieldPath.Child("realm"))...) + return field.ErrorList{field.Required(fieldPath.Child("realm"), "realm field must be present")} } + allErrs := validateRealm(jwt.Realm, fieldPath.Child("realm")) + // Use either JWT Secret or JWKS URI, they are mutually exclusive. if jwt.Secret == "" && jwt.JwksURI == "" { return append(allErrs, field.Required(fieldPath.Child("secret"), "either Secret or JwksURI must be present")) } - if jwt.Secret != "" && jwt.JwksURI != "" { return append(allErrs, field.Forbidden(fieldPath.Child("secret"), "only either of Secret or JwksURI can be used")) } - if jwt.KeyCache != "" && jwt.JwksURI == "" { - return append(allErrs, field.Required(fieldPath.Child("jwksURI"), "jwksURI must be present when keyCache is used.")) - } - - allErrs = append(allErrs, validateSecretName(jwt.Secret, fieldPath.Child("secret"))...) - - allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...) + // Verify a case when using JWT Secret + if jwt.Secret != "" { + allErrs = append(allErrs, validateSecretName(jwt.Secret, fieldPath.Child("secret"))...) + // jwt.Token is not required field. Verify it when provided. + if jwt.Token != "" { + allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...) + } - if jwt.JwksURI != "" { - allErrs = append(allErrs, validateURL(jwt.JwksURI, fieldPath.Child("jwksURI"))...) + // keyCache must not be present when using Secret + if jwt.KeyCache != "" { + allErrs = append(allErrs, field.Forbidden(fieldPath.Child("keyCache"), "key cache must not be used when using Secret")) + } + return allErrs } - if jwt.KeyCache != "" { + // Verify a case when using JWKS + if jwt.JwksURI != "" { + allErrs = append(allErrs, validateURL(jwt.JwksURI, fieldPath.Child("JwksURI"))...) allErrs = append(allErrs, validateTime(jwt.KeyCache, fieldPath.Child("keyCache"))...) + // jwt.Token is not required field. Verify it if it's provided. + if jwt.Token != "" { + allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...) + } + // keyCache must be present when using JWKS + if jwt.KeyCache == "" { + allErrs = append(allErrs, field.Required(fieldPath.Child("keyCache"), "key cache must be set, example value: 1h")) + } + return allErrs } - return allErrs } diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 4d715faec3..dcfe61d578 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -7,6 +7,247 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) +func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + policy *v1.Policy + }{ + { + name: "missing realm when using secret", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "", + Secret: "my-jwk", + }, + }, + }, + }, + { + name: "missing realm when using jwks from remote location", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "", + JwksURI: "https://mystore-jsonwebkeys.com", + KeyCache: "1h", + }, + }, + }, + }, + { + name: "missing secret and Jwks at the same time", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "my-realm", + }, + }, + }, + }, + { + name: "provided both Secret and JWKs at the same time", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "my-realm", + Secret: "my-secret", + JwksURI: "https://mystore-jsonwebkey.com", + }, + }, + }, + }, + + { + name: "keyCache must not be present when using Secret", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Secret: "my-jwk", + KeyCache: "1h", + }, + }, + }, + }, + { + name: "invalid keyCache time syntax", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + KeyCache: "bogus-time-value", + }, + }, + }, + }, + { + name: "missing keyCache when using JWKS", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + }, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := ValidatePolicy(tc.policy, true, false, false) + if err == nil { + t.Errorf("got no errors on invalid JWTAuth policy spec input") + } + }) + } +} + +func TestValidatePolicy_IsValidOnJWTPolicy(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + policy *v1.Policy + }{ + { + name: "with Secret and Token", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Secret: "my-secret", + Token: "$http_token", + }, + }, + }, + }, + { + name: "with Secret and without Token", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Secret: "my-jwk", + }, + }, + }, + }, + { + name: "with JWKS and Token", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + KeyCache: "1h", + JwksURI: "https://login.mydomain.com/keys", + Token: "$http_token", + }, + }, + }, + }, + { + name: "with JWKS and without Token", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + KeyCache: "1h", + JwksURI: "https://login.mydomain.com/keys", + }, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := ValidatePolicy(tc.policy, true, false, false) + if err != nil { + t.Errorf("want no errors, got %+v\n", err) + } + }) + } +} + +func TestValidatePolicy_RequiresKeyCacheValueForJWTPolicy(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + policy *v1.Policy + }{ + { + name: "keyCache in hours", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://foo.bar/certs", + KeyCache: "1h", + }, + }, + }, + }, + { + name: "keyCache in minutes", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://foo.bar/certs", + KeyCache: "120m", + }, + }, + }, + }, + { + name: "keyCache in seconds", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://foo.bar/certs", + KeyCache: "60000s", + }, + }, + }, + }, + { + name: "keyCache in days", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://foo.bar/certs", + KeyCache: "3d", + }, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := ValidatePolicy(tc.policy, true, false, false) + if err != nil { + t.Errorf("got error on valid JWT policy: %+v\n", err) + } + t.Log(err) + }) + } +} + func TestValidatePolicy_PassesOnValidInput(t *testing.T) { t.Parallel() tests := []struct { @@ -557,10 +798,14 @@ func TestValidateJWT_FailsOnInvalidInput(t *testing.T) { }, } for _, test := range tests { - allErrs := validateJWT(test.jwt, field.NewPath("jwt")) - if len(allErrs) == 0 { - t.Errorf("validateJWT() returned no errors for invalid input for the case of %v", test.msg) - } + test := test + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + allErrs := validateJWT(test.jwt, field.NewPath("jwt")) + if len(allErrs) == 0 { + t.Errorf("validateJWT() returned no errors for invalid input for the case of %v", test.msg) + } + }) } } @@ -635,6 +880,7 @@ func TestValidateRate_ErrorsOnInvalidInput(t *testing.T) { func TestValidatePositiveInt_PassesOnValidInput(t *testing.T) { t.Parallel() + validInput := []int{1, 2} for _, input := range validInput { @@ -658,16 +904,8 @@ func TestValidatePositiveInt_ErrorsOnInvalidInput(t *testing.T) { } } -func TestValidateRateLimitZoneSize_PassesOnValidInput(t *testing.T) { +func TestValidateRateLimitZoneSize_ErrorsOnInvalidInput(t *testing.T) { t.Parallel() - validInput := []string{"32", "32k", "32K", "10m"} - - for _, test := range validInput { - allErrs := validateRateLimitZoneSize(test, field.NewPath("size")) - if len(allErrs) != 0 { - t.Errorf("validateRateLimitZoneSize(%q) returned an error for valid input", test) - } - } invalidInput := []string{"", "31", "31k", "0", "0M"} @@ -679,8 +917,22 @@ func TestValidateRateLimitZoneSize_PassesOnValidInput(t *testing.T) { } } +func TestValidateRateLimitZoneSize_PassesOnValidInput(t *testing.T) { + t.Parallel() + + validInput := []string{"32", "32k", "32K", "10m"} + + for _, test := range validInput { + allErrs := validateRateLimitZoneSize(test, field.NewPath("size")) + if len(allErrs) != 0 { + t.Errorf("validateRateLimitZoneSize(%q) returned an error for valid input", test) + } + } +} + func TestValidateRateLimitZoneSize_FailsOnInvalidInput(t *testing.T) { t.Parallel() + invalidInput := []string{"", "31", "31k", "0", "0M"} for _, test := range invalidInput {