Adds support for other Cors directives

CORS annotations improvements

Cors improvements

Cors improevements

Cors improvements

Cors improvements
This commit is contained in:
Ricardo Pchevuzinske Katz 2017-10-19 18:03:02 -02:00
parent 99a355f25d
commit 2097676ca8
10 changed files with 277 additions and 83 deletions

View file

@ -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
}

View file

@ -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{},
}
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)
}
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()
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)
}
}

View file

@ -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),
@ -130,6 +130,7 @@ const (
sessionAffinity = "SessionAffinity"
serviceUpstream = "ServiceUpstream"
serverAlias = "Alias"
enableCors = "EnableCORS"
clientBodyBufferSize = "ClientBodyBufferSize"
certificateAuth = "CertificateAuth"
serverSnippet = "ServerSnippet"
@ -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) {

View file

@ -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 {
@ -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)
}
}
}

View file

@ -23,6 +23,7 @@ import (
"k8s.io/ingress-nginx/pkg/ingress"
"k8s.io/ingress-nginx/pkg/ingress/annotations/auth"
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
"k8s.io/ingress-nginx/pkg/ingress/annotations/cors"
"k8s.io/ingress-nginx/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/pkg/ingress/annotations/proxy"
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
@ -45,7 +46,7 @@ func TestMergeLocationAnnotations(t *testing.T) {
"Backend": "foo_backend",
"BasicDigestAuth": auth.BasicDigest{},
DeniedKeyName: &fakeError{},
"EnableCORS": true,
"EnableCORS": cors.CorsConfig{},
"ExternalAuth": authreq.External{},
"RateLimit": ratelimit.RateLimit{},
"Redirect": redirect.Redirect{},

View file

@ -29,6 +29,7 @@ import (
"k8s.io/ingress-nginx/pkg/ingress/annotations/auth"
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
"k8s.io/ingress-nginx/pkg/ingress/annotations/authtls"
"k8s.io/ingress-nginx/pkg/ingress/annotations/cors"
"k8s.io/ingress-nginx/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/pkg/ingress/annotations/proxy"
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
@ -293,9 +294,9 @@ type Location struct {
// Denied returns an error when this location cannot not be allowed
// Requesting a denied location should return HTTP code 403.
Denied error `json:"denied,omitempty"`
// EnableCORS indicates if path must support CORS
// CorsConfig returns the Cors Configration for the ingress rule
// +optional
EnableCORS bool `json:"enableCors,omitempty"`
CorsConfig cors.CorsConfig `json:"corsConfig,omitempty"`
// ExternalAuth indicates the access to this location requires
// authentication using an external provider
// +optional

View file

@ -355,7 +355,7 @@ func (l1 *Location) Equal(l2 *Location) bool {
if l1.Denied != l2.Denied {
return false
}
if l1.EnableCORS != l2.EnableCORS {
if !(&l1.CorsConfig).Equal(&l2.CorsConfig) {
return false
}
if !(&l1.ExternalAuth).Equal(&l2.ExternalAuth) {