Merge pull request #6217 from touchifyapp/@feature/cors-expose-headers
Add annotation to configure CORS Access-Control-Expose-Headers
This commit is contained in:
commit
6fd891f3df
6 changed files with 54 additions and 5 deletions
|
@ -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-methods](#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-max-age](#enable-cors)|number|
|
||||
|[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`
|
||||
- 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`
|
||||
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`
|
||||
|
|
|
@ -37,6 +37,7 @@ var (
|
|||
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
|
||||
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
|
||||
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
|
||||
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
|
||||
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
|
||||
backendProtocol = parser.GetAnnotationWithPrefix("backend-protocol")
|
||||
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
|
@ -201,12 +202,13 @@ func TestCors(t *testing.T) {
|
|||
headers string
|
||||
origin string
|
||||
credentials bool
|
||||
expose string
|
||||
}{
|
||||
{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", annotationCorsAllowCredentials: "false"}, true, defaultCorsMethods, defaultCorsHeaders, "*", false},
|
||||
{map[string]string{}, false, defaultCorsMethods, defaultCorsHeaders, "*", true},
|
||||
{nil, false, 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", 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{}, false, defaultCorsMethods, defaultCorsHeaders, "*", true, ""},
|
||||
{nil, false, defaultCorsMethods, defaultCorsHeaders, "*", true, ""},
|
||||
}
|
||||
|
||||
for _, foo := range fooAnns {
|
||||
|
|
|
@ -43,6 +43,9 @@ var (
|
|||
// Headers must contain valid values only (X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
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 {
|
||||
|
@ -56,6 +59,7 @@ type Config struct {
|
|||
CorsAllowMethods string `json:"corsAllowMethods"`
|
||||
CorsAllowHeaders string `json:"corsAllowHeaders"`
|
||||
CorsAllowCredentials bool `json:"corsAllowCredentials"`
|
||||
CorsExposeHeaders string `json:"corsExposeHeaders"`
|
||||
CorsMaxAge int `json:"corsMaxAge"`
|
||||
}
|
||||
|
||||
|
@ -75,6 +79,9 @@ func (c1 *Config) Equal(c2 *Config) bool {
|
|||
if c1.CorsMaxAge != c2.CorsMaxAge {
|
||||
return false
|
||||
}
|
||||
if c1.CorsExposeHeaders != c2.CorsExposeHeaders {
|
||||
return false
|
||||
}
|
||||
if c1.CorsAllowCredentials != c2.CorsAllowCredentials {
|
||||
return false
|
||||
}
|
||||
|
@ -125,6 +132,11 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
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)
|
||||
if err != nil {
|
||||
config.CorsMaxAge = defaultCorsMaxAge
|
||||
|
|
|
@ -73,6 +73,7 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
|
||||
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"
|
||||
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)
|
||||
}
|
||||
|
||||
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsMaxAge != 600 {
|
||||
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-methods")] = "GET, PATCH, $nginx"
|
||||
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"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -155,6 +161,10 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
|
|||
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 {
|
||||
t.Errorf("expected %v but returned %v", defaultCorsMaxAge, nginxCors.CorsMaxAge)
|
||||
}
|
||||
|
|
|
@ -830,6 +830,7 @@ stream {
|
|||
{{ 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-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 'Content-Type: text/plain charset=UTF-8';
|
||||
more_set_headers 'Content-Length: 0';
|
||||
|
@ -840,6 +841,7 @@ stream {
|
|||
{{ 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-Headers: {{ $cors.CorsAllowHeaders }}';
|
||||
{{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }}
|
||||
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -136,4 +136,20 @@ var _ = framework.DescribeAnnotation("cors-*", func() {
|
|||
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';")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue