Merge pull request #6217 from touchifyapp/@feature/cors-expose-headers

Add annotation to configure CORS Access-Control-Expose-Headers
This commit is contained in:
Kubernetes Prow Robot 2020-09-26 16:52:48 -07:00 committed by GitHub
commit 6fd891f3df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 5 deletions

View file

@ -48,6 +48,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/cors-allow-origin](#enable-cors)|string| |[nginx.ingress.kubernetes.io/cors-allow-origin](#enable-cors)|string|
|[nginx.ingress.kubernetes.io/cors-allow-methods](#enable-cors)|string| |[nginx.ingress.kubernetes.io/cors-allow-methods](#enable-cors)|string|
|[nginx.ingress.kubernetes.io/cors-allow-headers](#enable-cors)|string| |[nginx.ingress.kubernetes.io/cors-allow-headers](#enable-cors)|string|
|[nginx.ingress.kubernetes.io/cors-expose-headers](#enable-cors)|string|
|[nginx.ingress.kubernetes.io/cors-allow-credentials](#enable-cors)|"true" or "false"| |[nginx.ingress.kubernetes.io/cors-allow-credentials](#enable-cors)|"true" or "false"|
|[nginx.ingress.kubernetes.io/cors-max-age](#enable-cors)|number| |[nginx.ingress.kubernetes.io/cors-max-age](#enable-cors)|number|
|[nginx.ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"| |[nginx.ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
@ -336,6 +337,12 @@ CORS can be controlled with the following annotations:
- Default: `DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization` - Default: `DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization`
- Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"` - Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"`
* `nginx.ingress.kubernetes.io/cors-expose-headers`
controls which headers are exposed to response. This is a multi-valued field, separated by ',' and accepts
letters, numbers, _, - and *.
- Default: *empty*
- Example: `nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"`
* `nginx.ingress.kubernetes.io/cors-allow-origin` * `nginx.ingress.kubernetes.io/cors-allow-origin`
controls what's the accepted Origin for CORS. controls what's the accepted Origin for CORS.
This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port`

View file

@ -37,6 +37,7 @@ var (
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors") annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods") annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers") annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials") annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
backendProtocol = parser.GetAnnotationWithPrefix("backend-protocol") backendProtocol = parser.GetAnnotationWithPrefix("backend-protocol")
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS" defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
@ -201,12 +202,13 @@ func TestCors(t *testing.T) {
headers string headers string
origin string origin string
credentials bool credentials bool
expose string
}{ }{
{map[string]string{annotationCorsEnabled: "true"}, true, defaultCorsMethods, defaultCorsHeaders, "*", true}, {map[string]string{annotationCorsEnabled: "true"}, true, defaultCorsMethods, defaultCorsHeaders, "*", true, ""},
{map[string]string{annotationCorsEnabled: "true", annotationCorsAllowMethods: "POST, GET, OPTIONS", annotationCorsAllowHeaders: "$nginx_version", annotationCorsAllowCredentials: "false"}, true, "POST, GET, OPTIONS", defaultCorsHeaders, "*", false}, {map[string]string{annotationCorsEnabled: "true", annotationCorsAllowMethods: "POST, GET, OPTIONS", annotationCorsAllowHeaders: "$nginx_version", annotationCorsAllowCredentials: "false", annotationCorsExposeHeaders: "X-CustomResponseHeader"}, true, "POST, GET, OPTIONS", defaultCorsHeaders, "*", false, "X-CustomResponseHeader"},
{map[string]string{annotationCorsEnabled: "true", annotationCorsAllowCredentials: "false"}, true, defaultCorsMethods, defaultCorsHeaders, "*", false}, {map[string]string{annotationCorsEnabled: "true", annotationCorsAllowCredentials: "false"}, true, defaultCorsMethods, defaultCorsHeaders, "*", false, ""},
{map[string]string{}, false, defaultCorsMethods, defaultCorsHeaders, "*", true}, {map[string]string{}, false, defaultCorsMethods, defaultCorsHeaders, "*", true, ""},
{nil, false, defaultCorsMethods, defaultCorsHeaders, "*", true}, {nil, false, defaultCorsMethods, defaultCorsHeaders, "*", true, ""},
} }
for _, foo := range fooAnns { for _, foo := range fooAnns {

View file

@ -43,6 +43,9 @@ var (
// Headers must contain valid values only (X-HEADER12, X-ABC) // Headers must contain valid values only (X-HEADER12, X-ABC)
// May contain or not spaces between each Header // May contain or not spaces between each Header
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`) corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`)
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC)
// May contain or not spaces between each Header
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`)
) )
type cors struct { type cors struct {
@ -56,6 +59,7 @@ type Config struct {
CorsAllowMethods string `json:"corsAllowMethods"` CorsAllowMethods string `json:"corsAllowMethods"`
CorsAllowHeaders string `json:"corsAllowHeaders"` CorsAllowHeaders string `json:"corsAllowHeaders"`
CorsAllowCredentials bool `json:"corsAllowCredentials"` CorsAllowCredentials bool `json:"corsAllowCredentials"`
CorsExposeHeaders string `json:"corsExposeHeaders"`
CorsMaxAge int `json:"corsMaxAge"` CorsMaxAge int `json:"corsMaxAge"`
} }
@ -75,6 +79,9 @@ func (c1 *Config) Equal(c2 *Config) bool {
if c1.CorsMaxAge != c2.CorsMaxAge { if c1.CorsMaxAge != c2.CorsMaxAge {
return false return false
} }
if c1.CorsExposeHeaders != c2.CorsExposeHeaders {
return false
}
if c1.CorsAllowCredentials != c2.CorsAllowCredentials { if c1.CorsAllowCredentials != c2.CorsAllowCredentials {
return false return false
} }
@ -125,6 +132,11 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
config.CorsAllowCredentials = true config.CorsAllowCredentials = true
} }
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing)
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) {
config.CorsExposeHeaders = ""
}
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing) config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing)
if err != nil { if err != nil {
config.CorsMaxAge = defaultCorsMaxAge config.CorsMaxAge = defaultCorsMaxAge

View file

@ -73,6 +73,7 @@ func TestIngressCorsConfigValid(t *testing.T) {
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false" data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH" data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443" data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader"
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600" data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600"
ing.SetAnnotations(data) ing.SetAnnotations(data)
@ -106,6 +107,10 @@ func TestIngressCorsConfigValid(t *testing.T) {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin) t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin)
} }
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
}
if nginxCors.CorsMaxAge != 600 { if nginxCors.CorsMaxAge != 600 {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge) t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge)
} }
@ -122,6 +127,7 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no" data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no"
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx" data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx"
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443" data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress"
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd" data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd"
ing.SetAnnotations(data) ing.SetAnnotations(data)
@ -155,6 +161,10 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
t.Errorf("expected %v but returned %v", "*", nginxCors.CorsAllowOrigin) t.Errorf("expected %v but returned %v", "*", nginxCors.CorsAllowOrigin)
} }
if nginxCors.CorsExposeHeaders != "" {
t.Errorf("expected %v but returned %v", "", nginxCors.CorsExposeHeaders)
}
if nginxCors.CorsMaxAge != defaultCorsMaxAge { if nginxCors.CorsMaxAge != defaultCorsMaxAge {
t.Errorf("expected %v but returned %v", defaultCorsMaxAge, nginxCors.CorsMaxAge) t.Errorf("expected %v but returned %v", defaultCorsMaxAge, nginxCors.CorsMaxAge)
} }

View file

@ -830,6 +830,7 @@ stream {
{{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }} {{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }}
more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}'; more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}';
more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}';
{{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }}
more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}'; more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}';
more_set_headers 'Content-Type: text/plain charset=UTF-8'; more_set_headers 'Content-Type: text/plain charset=UTF-8';
more_set_headers 'Content-Length: 0'; more_set_headers 'Content-Length: 0';
@ -840,6 +841,7 @@ stream {
{{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }} {{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }}
more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}'; more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}';
more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}';
{{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }}
{{ end }} {{ end }}

View file

@ -136,4 +136,20 @@ var _ = framework.DescribeAnnotation("cors-*", func() {
return strings.Contains(server, "more_set_headers 'Access-Control-Allow-Headers: DNT, User-Agent';") return strings.Contains(server, "more_set_headers 'Access-Control-Allow-Headers: DNT, User-Agent';")
}) })
}) })
ginkgo.It("should expose headers for cors", func() {
host := "cors.foo.com"
annotations := map[string]string{
"nginx.ingress.kubernetes.io/enable-cors": "true",
"nginx.ingress.kubernetes.io/cors-expose-headers": "X-CustomResponseHeader, X-CustomSecondHeader",
}
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
f.EnsureIngress(ing)
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "more_set_headers 'Access-Control-Expose-Headers: X-CustomResponseHeader, X-CustomSecondHeader';")
})
})
}) })