Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cors features improvements #1553

Merged
merged 1 commit into from
Oct 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ Key:
| `base-url-scheme` | Specify the scheme of the `<base>` tags. | | nginx
| `preserve-host` | Whether to pass the client request host (`true`) or the origin hostname (`false`) in the HTTP Host field. | | trafficserver

## CORS Related
| Name | Meaning | Default | Controller
| --- | --- | --- | --- |
| `enable-cors` | Enable CORS headers in response. | false | nginx, voyager
| `cors-allow-origin` | Specifies the Origin allowed in CORS (Access-Control-Allow-Origin) | * | nginx
| `cors-allow-headers` | Specifies the Headers allowed in CORS (Access-Control-Allow-Headers) | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization | nginx
| `cors-allow-methods` | Specifies the Methods allowed in CORS (Access-Control-Allow-Methods) | GET, PUT, POST, DELETE, PATCH, OPTIONS | nginx
| `cors-allow-credentials` | Specifies the Access-Control-Allow-Credentials | true | nginx

## Miscellaneous

| Name | Meaning | Default | Controller
| --- | --- | --- | --- |
| `configuration-snippet` | Arbitrary text to put in the generated configuration file. | | nginx
| `enable-cors` | Enable CORS headers in response. | | nginx, voyager
| `limit-connections` | Limit concurrent connections per IP address[1]. | | nginx, voyager
| `limit-rps` | Limit requests per second per IP address[1]. | | nginx, voyager
| `limit-rpm` | Limit requests per minute per IP address. | | nginx, voyager
Expand Down
24 changes: 24 additions & 0 deletions docs/user-guide/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ The following annotations are supported:
|[ingress.kubernetes.io/configuration-snippet](#configuration-snippet)|string|
|[ingress.kubernetes.io/default-backend](#default-backend)|string|
|[ingress.kubernetes.io/enable-cors](#enable-cors)|true or false|
|[ingress.kubernetes.io/cors-allow-origin](#enable-cors)|string|
|[ingress.kubernetes.io/cors-allow-methods](#enable-cors)|string|
|[ingress.kubernetes.io/cors-allow-headers](#enable-cors)|string|
|[ingress.kubernetes.io/cors-allow-credentials](#enable-cors)|true or false|
|[ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false|
|[ingress.kubernetes.io/from-to-www-redirect](#redirect-from-to-www)|true or false|
|[ingress.kubernetes.io/limit-connections](#rate-limiting)|number|
Expand Down Expand Up @@ -162,6 +166,26 @@ This is a global configuration for the ingress controller. In some cases could b
### Enable CORS

To enable Cross-Origin Resource Sharing (CORS) in an Ingress rule add the annotation `ingress.kubernetes.io/enable-cors: "true"`. This will add a section in the server location enabling this functionality.

CORS can be controlled with the following annotations:

* `ingress.kubernetes.io/cors-allow-methods` controls which methods are accepted. This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case).

Example: `ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"`

* `ingress.kubernetes.io/cors-allow-headers` controls which headers are accepted. This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -.

Example: `ingress.kubernetes.io/cors-allow-methods: "X-Forwarded-For, X-app123-XPTO"`

* `ingress.kubernetes.io/cors-allow-origin` controls what's the accepted Origin for CORS and defaults to '*'. This is a single field value, with the following format: http(s)://origin-site.com or http(s)://origin-site.com:port

Example: `ingress.kubernetes.io/cors-allow-origin: "https://origin-site.com:4443"`

* `ingress.kubernetes.io/cors-allow-credentials` controls if credentials can be passed during CORS operations.

Example: `ingress.kubernetes.io/cors-allow-credentials: "true"`


For more information please check https://enable-cors.org/server_nginx.html

### Server Alias
Expand Down
91 changes: 89 additions & 2 deletions pkg/ingress/annotations/cors/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,112 @@ limitations under the License.
package cors

import (
"regexp"

extensions "k8s.io/api/extensions/v1beta1"

"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
)

const (
annotation = "ingress.kubernetes.io/enable-cors"
annotationCorsEnabled = "ingress.kubernetes.io/enable-cors"
annotationCorsAllowOrigin = "ingress.kubernetes.io/cors-allow-origin"
annotationCorsAllowMethods = "ingress.kubernetes.io/cors-allow-methods"
annotationCorsAllowHeaders = "ingress.kubernetes.io/cors-allow-headers"
annotationCorsAllowCredentials = "ingress.kubernetes.io/cors-allow-credentials"
// Default values
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
)

var (
// Regex are defined here to prevent information leak, if user tries to set anything not valid
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc)
// Origin must contain a http/s Origin (including or not the port) or the value '*'
corsOriginRegex = regexp.MustCompile(`^(https?://[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`)
// Method must contain valid methods list (PUT, GET, POST, BLA)
// May contain or not spaces between each verb
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`)
// 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?)+$`)
)

type cors struct {
}

// CorsConfig contains the Cors configuration to be used in the Ingress
type CorsConfig struct {
CorsEnabled bool `json:"corsEnabled"`
CorsAllowOrigin string `json:"corsAllowOrigin"`
CorsAllowMethods string `json:"corsAllowMethods"`
CorsAllowHeaders string `json:"corsAllowHeaders"`
CorsAllowCredentials bool `json:"corsAllowCredentials"`
}

// NewParser creates a new CORS annotation parser
func NewParser() parser.IngressAnnotation {
return cors{}
}

// Equal tests for equality between two External types
func (c1 *CorsConfig) Equal(c2 *CorsConfig) bool {
if c1 == c2 {
return true
}
if c1 == nil || c2 == nil {
return false
}
if c1.CorsAllowCredentials != c2.CorsAllowCredentials {
return false
}
if c1.CorsAllowHeaders != c2.CorsAllowHeaders {
return false
}
if c1.CorsAllowOrigin != c2.CorsAllowOrigin {
return false
}
if c1.CorsEnabled != c2.CorsEnabled {
return false
}

return true
}

// Parse parses the annotations contained in the ingress
// rule used to indicate if the location/s should allows CORS
func (a cors) Parse(ing *extensions.Ingress) (interface{}, error) {
return parser.GetBoolAnnotation(annotation, ing)
corsenabled, err := parser.GetBoolAnnotation(annotationCorsEnabled, ing)
if err != nil {
corsenabled = false
}

corsalloworigin, err := parser.GetStringAnnotation(annotationCorsAllowOrigin, ing)
if err != nil || corsalloworigin == "" || !corsOriginRegex.MatchString(corsalloworigin) {
corsalloworigin = "*"
}

corsallowheaders, err := parser.GetStringAnnotation(annotationCorsAllowHeaders, ing)
if err != nil || corsallowheaders == "" || !corsHeadersRegex.MatchString(corsallowheaders) {
corsallowheaders = defaultCorsHeaders
}

corsallowmethods, err := parser.GetStringAnnotation(annotationCorsAllowMethods, ing)
if err != nil || corsallowmethods == "" || !corsMethodsRegex.MatchString(corsallowmethods) {
corsallowmethods = defaultCorsMethods
}

corsallowcredentials, err := parser.GetBoolAnnotation(annotationCorsAllowCredentials, ing)
if err != nil {
corsallowcredentials = true
}

return &CorsConfig{
CorsEnabled: corsenabled,
CorsAllowOrigin: corsalloworigin,
CorsAllowHeaders: corsallowheaders,
CorsAllowMethods: corsallowmethods,
CorsAllowCredentials: corsallowcredentials,
}, nil

}
87 changes: 60 additions & 27 deletions pkg/ingress/annotations/cors/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,75 @@ import (
api "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

const (
notCorsAnnotation = "ingress.kubernetes.io/enable-not-cors"
)

func TestParse(t *testing.T) {
ap := NewParser()
if ap == nil {
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
func buildIngress() *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}

testCases := []struct {
annotations map[string]string
expected bool
}{
{map[string]string{annotation: "true"}, true},
{map[string]string{annotation: "false"}, false},
{map[string]string{notCorsAnnotation: "true"}, false},
{map[string]string{}, false},
{nil, false},
}

ing := &extensions.Ingress{
return &extensions.Ingress{
ObjectMeta: meta_v1.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}

func TestIngressCorsConfig(t *testing.T) {
ing := buildIngress()

for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
if result != testCase.expected {
t.Errorf("expected %t but returned %t, annotations: %s", testCase.expected, result, testCase.annotations)
}
data := map[string]string{}
data[annotationCorsEnabled] = "true"
data[annotationCorsAllowHeaders] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
data[annotationCorsAllowCredentials] = "false"
data[annotationCorsAllowMethods] = "PUT, GET,OPTIONS, PATCH, $nginx_version"
data[annotationCorsAllowOrigin] = "https://origin123.test.com:4443"
ing.SetAnnotations(data)

corst, _ := NewParser().Parse(ing)
nginxCors, ok := corst.(*CorsConfig)
if !ok {
t.Errorf("expected a Config type")
}

if nginxCors.CorsEnabled != true {
t.Errorf("expected cors enabled but returned %v", nginxCors.CorsEnabled)
}

if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" {
t.Errorf("expected headers not found. Found %v", nginxCors.CorsAllowHeaders)
}

if nginxCors.CorsAllowMethods != "GET, PUT, POST, DELETE, PATCH, OPTIONS" {
t.Errorf("expected default methods, but got %v", nginxCors.CorsAllowMethods)
}

if nginxCors.CorsAllowOrigin != "https://origin123.test.com:4443" {
t.Errorf("expected origin https://origin123.test.com:4443, but got %v", nginxCors.CorsAllowOrigin)
}

}
8 changes: 7 additions & 1 deletion pkg/ingress/controller/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
"BasicDigestAuth": auth.NewParser(auth.AuthDirectory, cfg),
"ExternalAuth": authreq.NewParser(),
"CertificateAuth": authtls.NewParser(cfg),
"EnableCORS": cors.NewParser(),
"CorsConfig": cors.NewParser(),
"HealthCheck": healthcheck.NewParser(cfg),
"Whitelist": ipwhitelist.NewParser(cfg),
"UsePortInRedirects": portinredirect.NewParser(cfg),
Expand Down Expand Up @@ -130,6 +130,7 @@ const (
sessionAffinity = "SessionAffinity"
serviceUpstream = "ServiceUpstream"
serverAlias = "Alias"
enableCors = "EnableCORS"
clientBodyBufferSize = "ClientBodyBufferSize"
certificateAuth = "CertificateAuth"
serverSnippet = "ServerSnippet"
Expand Down Expand Up @@ -175,6 +176,11 @@ func (e *annotationExtractor) SessionAffinity(ing *extensions.Ingress) *sessiona
return val.(*sessionaffinity.AffinityConfig)
}

func (e *annotationExtractor) Cors(ing *extensions.Ingress) *cors.CorsConfig {
val, _ := e.annotations[enableCors].Parse(ing)
return val.(*cors.CorsConfig)
}

func (e *annotationExtractor) CertificateAuth(ing *extensions.Ingress) *authtls.AuthSSLConfig {
val, err := e.annotations[certificateAuth].Parse(ing)
if errors.IsMissingAnnotations(err) {
Expand Down
76 changes: 67 additions & 9 deletions pkg/ingress/controller/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ import (
)

const (
annotationSecureUpstream = "ingress.kubernetes.io/secure-backends"
annotationSecureVerifyCACert = "ingress.kubernetes.io/secure-verify-ca-secret"
annotationUpsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
annotationUpsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
annotationPassthrough = "ingress.kubernetes.io/ssl-passthrough"
annotationAffinityType = "ingress.kubernetes.io/affinity"
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
annotationAffinityCookieHash = "ingress.kubernetes.io/session-cookie-hash"
annotationUpstreamHashBy = "ingress.kubernetes.io/upstream-hash-by"
annotationSecureUpstream = "ingress.kubernetes.io/secure-backends"
annotationSecureVerifyCACert = "ingress.kubernetes.io/secure-verify-ca-secret"
annotationUpsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
annotationUpsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
annotationPassthrough = "ingress.kubernetes.io/ssl-passthrough"
annotationAffinityType = "ingress.kubernetes.io/affinity"
annotationCorsEnabled = "ingress.kubernetes.io/enable-cors"
annotationCorsAllowOrigin = "ingress.kubernetes.io/cors-allow-origin"
annotationCorsAllowMethods = "ingress.kubernetes.io/cors-allow-methods"
annotationCorsAllowHeaders = "ingress.kubernetes.io/cors-allow-headers"
annotationCorsAllowCredentials = "ingress.kubernetes.io/cors-allow-credentials"
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
annotationAffinityCookieHash = "ingress.kubernetes.io/session-cookie-hash"
annotationUpstreamHashBy = "ingress.kubernetes.io/upstream-hash-by"
)

type mockCfg struct {
Expand Down Expand Up @@ -293,3 +300,54 @@ func TestAffinitySession(t *testing.T) {
}
}
}

func TestCors(t *testing.T) {
ec := newAnnotationExtractor(mockCfg{})
ing := buildIngress()

fooAnns := []struct {
annotations map[string]string
corsenabled bool
methods string
headers string
origin string
credentials bool
}{
{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},
}

for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Cors(ing)
t.Logf("Testing pass %v %v %v %v %v", foo.corsenabled, foo.methods, foo.headers, foo.origin, foo.credentials)
if r == nil {
t.Errorf("Returned nil but expected a Cors.CorsConfig")
continue
}

if r.CorsEnabled != foo.corsenabled {
t.Errorf("Returned %v but expected %v for Cors Enabled", r.CorsEnabled, foo.corsenabled)
}

if r.CorsAllowHeaders != foo.headers {
t.Errorf("Returned %v but expected %v for Cors Headers", r.CorsAllowHeaders, foo.headers)
}

if r.CorsAllowMethods != foo.methods {
t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowMethods, foo.methods)
}

if r.CorsAllowOrigin != foo.origin {
t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowOrigin, foo.origin)
}

if r.CorsAllowCredentials != foo.credentials {
t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowCredentials, foo.credentials)
}

}
}
Loading