diff --git a/controllers/nginx/Makefile b/controllers/nginx/Makefile index 8878ecb4f..d4265c25b 100644 --- a/controllers/nginx/Makefile +++ b/controllers/nginx/Makefile @@ -3,7 +3,7 @@ all: push BUILDTAGS= # Use the 0.0 tag for testing, it shouldn't clobber any release builds -RELEASE?=0.9.0-beta.1 +RELEASE?=0.9.0-beta.1-1 PREFIX?=gcr.io/google_containers/nginx-ingress-controller GOOS?=linux diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index cc21dccac..4185bcc14 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -134,6 +134,7 @@ var ( }, "buildLocation": buildLocation, "buildAuthLocation": buildAuthLocation, + "buildAuthResponseHeaders": buildAuthResponseHeaders, "buildProxyPass": buildProxyPass, "buildRateLimitZones": buildRateLimitZones, "buildRateLimit": buildRateLimit, @@ -259,6 +260,27 @@ func buildAuthLocation(input interface{}) string { return fmt.Sprintf("/_external-auth-%v", str) } +func buildAuthResponseHeaders(input interface{}) []string { + location, ok := input.(*ingress.Location) + res := []string{} + if !ok { + return res + } + + if len(location.ExternalAuth.ResponseHeaders) == 0 { + return res + } + + for i, h := range location.ExternalAuth.ResponseHeaders { + hvar := strings.ToLower(h) + hvar = strings.NewReplacer("-", "_").Replace(hvar) + res = append(res, fmt.Sprintf("auth_request_set $authHeader%v $upstream_http_%v;", i, hvar)) + res = append(res, fmt.Sprintf("proxy_set_header '%v' $authHeader%v;", h, i)) + } + return res +} + + // buildProxyPass produces the proxy pass string, if the ingress has redirects // (specified through the ingress.kubernetes.io/rewrite-to annotation) // If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will diff --git a/controllers/nginx/pkg/template/template_test.go b/controllers/nginx/pkg/template/template_test.go index fde02e3ef..ed48fb707 100644 --- a/controllers/nginx/pkg/template/template_test.go +++ b/controllers/nginx/pkg/template/template_test.go @@ -21,6 +21,7 @@ import ( "os" "path" "strings" + "reflect" "testing" "io/ioutil" @@ -28,6 +29,7 @@ import ( "k8s.io/ingress/controllers/nginx/pkg/config" "k8s.io/ingress/core/pkg/ingress" "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" + "k8s.io/ingress/core/pkg/ingress/annotations/authreq" ) var ( @@ -105,6 +107,23 @@ func TestBuildProxyPass(t *testing.T) { } } +func TestBuildAuthResponseHeaders(t *testing.T) { + loc := &ingress.Location{ + ExternalAuth: authreq.External{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}}, + } + headers := buildAuthResponseHeaders(loc) + expected := []string{ + "auth_request_set $authHeader0 $upstream_http_h1;", + "proxy_set_header 'h1' $authHeader0;", + "auth_request_set $authHeader1 $upstream_http_h_with_caps_and_dashes;", + "proxy_set_header 'H-With-Caps-And-Dashes' $authHeader1;", + } + + if !reflect.DeepEqual(expected, headers) { + t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, headers) + } +} + func TestTemplateWithData(t *testing.T) { pwd, _ := os.Getwd() f, err := os.Open(path.Join(pwd, "../../test/data/config.json")) diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index 2c4d3cc2e..ed17a8dbb 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -258,6 +258,9 @@ http { {{ if not (empty $authPath) }} # this location requires authentication auth_request {{ $authPath }}; + {{- range $idx, $line := buildAuthResponseHeaders $location }} + {{ $line }} + {{- end }} {{ end }} {{ if (and (not (empty $server.SSLCertificate)) $location.Redirect.SSLRedirect) }} diff --git a/core/pkg/ingress/annotations/authreq/main.go b/core/pkg/ingress/annotations/authreq/main.go index 560a73868..b5bfd1dde 100644 --- a/core/pkg/ingress/annotations/authreq/main.go +++ b/core/pkg/ingress/annotations/authreq/main.go @@ -19,6 +19,7 @@ package authreq import ( "net/url" "strings" + "regexp" "k8s.io/kubernetes/pkg/apis/extensions" @@ -28,20 +29,23 @@ import ( const ( // external URL that provides the authentication - authURL = "ingress.kubernetes.io/auth-url" - authMethod = "ingress.kubernetes.io/auth-method" - authBody = "ingress.kubernetes.io/auth-send-body" + authURL = "ingress.kubernetes.io/auth-url" + authMethod = "ingress.kubernetes.io/auth-method" + authBody = "ingress.kubernetes.io/auth-send-body" + authHeaders = "ingress.kubernetes.io/auth-response-headers" ) // External returns external authentication configuration for an Ingress rule type External struct { - URL string `json:"url"` - Method string `json:"method"` - SendBody bool `json:"sendBody"` + URL string `json:"url"` + Method string `json:"method"` + SendBody bool `json:"sendBody"` + ResponseHeaders []string `json:"responseHeaders"` } var ( methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"} + headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`) ) func validMethod(method string) bool { @@ -57,6 +61,10 @@ func validMethod(method string) bool { return false } +func validHeader(header string) bool { + return headerRegexp.Match([]byte(header)) +} + type authReq struct { } @@ -97,11 +105,28 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { return nil, ing_errors.NewLocationDenied("invalid HTTP method") } + h := []string{} + hstr, _ := parser.GetStringAnnotation(authHeaders, ing) + if len(hstr) != 0 { + + harr := strings.Split(hstr, ",") + for _, header := range harr { + header := strings.TrimSpace(header) + if len(header) > 0 { + if !validHeader(header) { + return nil, ing_errors.NewLocationDenied("invalid headers list") + } + h = append(h, header) + } + } + } + sb, _ := parser.GetBoolAnnotation(authBody, ing) return &External{ URL: str, Method: m, SendBody: sb, + ResponseHeaders: h, }, nil } diff --git a/core/pkg/ingress/annotations/authreq/main_test.go b/core/pkg/ingress/annotations/authreq/main_test.go index 696d8bdc0..ba92d406e 100644 --- a/core/pkg/ingress/annotations/authreq/main_test.go +++ b/core/pkg/ingress/annotations/authreq/main_test.go @@ -19,6 +19,7 @@ package authreq import ( "fmt" "testing" + "reflect" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/extensions" @@ -109,3 +110,48 @@ func TestAnnotations(t *testing.T) { } } } + +func TestHeaderAnnotations(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + ing.SetAnnotations(data) + + tests := []struct { + title string + url string + headers string + parsedHeaders []string + expErr bool + }{ + {"single header", "http://goog.url", "h1", []string{"h1"}, false}, + {"nothing", "http://goog.url", "", []string{}, false}, + {"spaces", "http://goog.url", " ", []string{}, false}, + {"two headers", "http://goog.url", "1,2", []string{"1", "2"}, false}, + {"two headers and empty entries", "http://goog.url", ",1,,2,", []string{"1", "2"}, false}, + {"header with spaces", "http://goog.url", "1 2", []string{}, true}, + {"header with other bad symbols", "http://goog.url", "1+2", []string{}, true}, + } + + for _, test := range tests { + data[authURL] = test.url + data[authHeaders] = test.headers + + i, err := NewParser().Parse(ing) + if test.expErr { + if err == nil { + t.Errorf("%v: expected error but retuned nil", err.Error()) + } + continue + } + + u, ok := i.(*External) + if !ok { + t.Errorf("%v: expected an External type", test.title) + } + + if !reflect.DeepEqual(u.ResponseHeaders, test.parsedHeaders) { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.headers, u.ResponseHeaders) + } + } +}