Enable Customization of Auth Request Redirect (#1993)

Adds the 'nginx.ingress.kubernetes.io/auth-request-redirect'
annotation, which allows the customization of the
'X-Auth-Request-Redirect' Header. Fixes: #1979
This commit is contained in:
Fernando Diaz 2018-01-27 18:32:08 -06:00 committed by Manuel Alejandro de Brito Fontes
parent efec983ed4
commit d1ae7ff29c
7 changed files with 89 additions and 48 deletions

View file

@ -7,9 +7,11 @@ Use an external service (Basic Auth) located in `https://httpbin.org`
```
$ kubectl create -f ingress.yaml
ingress "external-auth" created
$ kubectl get ing external-auth
NAME HOSTS ADDRESS PORTS AGE
external-auth external-auth-01.sample.com 172.17.4.99 80 13s
$ kubectl get ing external-auth -o yaml
apiVersion: extensions/v1beta1
kind: Ingress

View file

@ -282,12 +282,21 @@ For more information please see http://nginx.org/en/docs/http/ngx_http_core_modu
### External Authentication
To use an existing service that provides authentication the Ingress rule can be annotated with `nginx.ingress.kubernetes.io/auth-url` to indicate the URL where the HTTP request should be sent.
Additionally it is possible to set `nginx.ingress.kubernetes.io/auth-method` to specify the HTTP method to use (GET or POST).
```yaml
nginx.ingress.kubernetes.io/auth-url: "URL to the authentication service"
```
Additionally it is possible to set:
`nginx.ingress.kubernetes.io/auth-method`: `<Method>` to specify the HTTP method to use.
`nginx.ingress.kubernetes.io/auth-signin`: `<SignIn_URL>` to specify the location of the error page.
`nginx.ingress.kubernetes.io/auth-response-headers`: `<Response_Header_1, ..., Response_Header_n>` to specify headers to pass to backend once authorization request completes.
`nginx.ingress.kuberentes.io/auth-request-redirect`: `<Request_Redirect_URL>` to specify the X-Auth-Request-Redirect header value.
Please check the [external-auth](../examples/auth/external-auth/README.md) example.
### Rate limiting

View file

@ -36,6 +36,7 @@ type Config struct {
SigninURL string `json:"signinUrl"`
Method string `json:"method"`
ResponseHeaders []string `json:"responseHeaders,omitEmpty"`
RequestRedirect string `json:"requestRedirect"`
}
// Equal tests for equality between two Config types
@ -58,10 +59,6 @@ func (e1 *Config) Equal(e2 *Config) bool {
if e1.Method != e2.Method {
return false
}
if e1.Method != e2.Method {
return false
}
for _, ep1 := range e1.ResponseHeaders {
found := false
for _, ep2 := range e2.ResponseHeaders {
@ -74,6 +71,9 @@ func (e1 *Config) Equal(e2 *Config) bool {
return false
}
}
if e1.RequestRedirect != e2.RequestRedirect {
return false
}
return true
}
@ -112,41 +112,40 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an Config URL as source for authentication
func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
str, err := parser.GetStringAnnotation("auth-url", ing)
// Required Parameters
urlString, err := parser.GetStringAnnotation("auth-url", ing)
if err != nil {
return nil, err
}
if str == "" {
if urlString == "" {
return nil, ing_errors.NewLocationDenied("an empty string is not a valid URL")
}
signin, _ := parser.GetStringAnnotation("auth-signin", ing)
ur, err := url.Parse(str)
authUrl, err := url.Parse(urlString)
if err != nil {
return nil, err
}
if ur.Scheme == "" {
if authUrl.Scheme == "" {
return nil, ing_errors.NewLocationDenied("url scheme is empty")
}
if ur.Host == "" {
if authUrl.Host == "" {
return nil, ing_errors.NewLocationDenied("url host is empty")
}
if strings.Contains(ur.Host, "..") {
if strings.Contains(authUrl.Host, "..") {
return nil, ing_errors.NewLocationDenied("invalid url host")
}
m, _ := parser.GetStringAnnotation("auth-method", ing)
if len(m) != 0 && !validMethod(m) {
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
if len(authMethod) != 0 && !validMethod(authMethod) {
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
}
h := []string{}
// Optional Parameters
signIn, _ := parser.GetStringAnnotation("auth-signin", ing)
responseHeaders := []string{}
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
if len(hstr) != 0 {
harr := strings.Split(hstr, ",")
for _, header := range harr {
header = strings.TrimSpace(header)
@ -154,16 +153,19 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
if !validHeader(header) {
return nil, ing_errors.NewLocationDenied("invalid headers list")
}
h = append(h, header)
responseHeaders = append(responseHeaders, header)
}
}
}
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
return &Config{
URL: str,
Host: ur.Hostname(),
SigninURL: signin,
Method: m,
ResponseHeaders: h,
URL: urlString,
Host: authUrl.Hostname(),
SigninURL: signIn,
Method: authMethod,
ResponseHeaders: responseHeaders,
RequestRedirect: requestRedirect,
}, nil
}

View file

@ -76,21 +76,24 @@ func TestAnnotations(t *testing.T) {
url string
signinURL string
method string
requestRedirect string
expErr bool
}{
{"empty", "", "", "", true},
{"no scheme", "bar", "bar", "", true},
{"invalid host", "http://", "http://", "", true},
{"invalid host (multiple dots)", "http://foo..bar.com", "http://foo..bar.com", "", true},
{"valid URL", "http://bar.foo.com/external-auth", "http://bar.foo.com/external-auth", "", false},
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "POST", false},
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", false},
{"empty", "", "", "", "", true},
{"no scheme", "bar", "bar", "", "", true},
{"invalid host", "http://", "http://", "", "", true},
{"invalid host (multiple dots)", "http://foo..bar.com", "http://foo..bar.com", "", "", true},
{"valid URL", "http://bar.foo.com/external-auth", "http://bar.foo.com/external-auth", "", "", false},
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "POST", "", false},
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", "", false},
{"valid URL - request redirect", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", "http://foo.com/redirect-me", false},
}
for _, test := range tests {
data[parser.GetAnnotationWithPrefix("auth-url")] = test.url
data[parser.GetAnnotationWithPrefix("auth-signin")] = test.signinURL
data[parser.GetAnnotationWithPrefix("auth-method")] = fmt.Sprintf("%v", test.method)
data[parser.GetAnnotationWithPrefix("auth-request-redirect")] = test.requestRedirect
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if test.expErr {
@ -112,6 +115,9 @@ func TestAnnotations(t *testing.T) {
if u.Method != test.method {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
}
if u.RequestRedirect != test.requestRedirect {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.requestRedirect, u.RequestRedirect)
}
}
}

View file

@ -214,7 +214,6 @@ func buildLocation(input interface{}) string {
return path
}
// TODO: Needs Unit Tests
func buildAuthLocation(input interface{}) string {
location, ok := input.(*ingress.Location)
if !ok {
@ -227,7 +226,7 @@ func buildAuthLocation(input interface{}) string {
}
str := base64.URLEncoding.EncodeToString([]byte(location.Path))
// avoid locations containing the = char
// removes "=" after encoding
str = strings.Replace(str, "=", "", -1)
return fmt.Sprintf("/_external-auth-%v", str)
}

View file

@ -26,6 +26,8 @@ import (
"strings"
"testing"
"encoding/base64"
"fmt"
"k8s.io/ingress-nginx/internal/file"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
@ -172,6 +174,26 @@ func TestBuildProxyPass(t *testing.T) {
}
}
func TestBuildAuthLocation(t *testing.T) {
authURL := "foo.com/auth"
loc := &ingress.Location{
ExternalAuth: authreq.Config{
URL: authURL,
},
Path: "/cat",
}
str := buildAuthLocation(loc)
encodedAuthURL := strings.Replace(base64.URLEncoding.EncodeToString([]byte(loc.Path)), "=", "", -1)
expected := fmt.Sprintf("/_external-auth-%v", encodedAuthURL)
if str != expected {
t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, str)
}
}
func TestBuildAuthResponseHeaders(t *testing.T) {
loc := &ingress.Location{
ExternalAuth: authreq.Config{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}},

View file

@ -316,7 +316,6 @@ http {
{{ end }}
upstream {{ $upstream.Name }} {
{{ if $upstream.UpstreamHashBy }}
hash {{ $upstream.UpstreamHashBy }} consistent;
@ -623,7 +622,6 @@ stream {
more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }};{{ if $all.Cfg.HSTSPreload }} preload{{ end }}";
{{ end }}
{{ if not (empty $server.CertificateAuth.CAFileName) }}
# PEM sha: {{ $server.CertificateAuth.PemSHA }}
ssl_client_certificate {{ $server.CertificateAuth.CAFileName }};
@ -648,7 +646,7 @@ stream {
}
{{ end }}
{{ if not (empty $authPath) }}
{{ if $authPath }}
location = {{ $authPath }} {
internal;
set $proxy_upstream_name "external-authentication";
@ -656,7 +654,7 @@ stream {
proxy_pass_request_body off;
proxy_set_header Content-Length "";
{{ if not (empty $location.ExternalAuth.Method) }}
{{ if $location.ExternalAuth.Method }}
proxy_method {{ $location.ExternalAuth.Method }};
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Scheme $pass_access_scheme;
@ -665,9 +663,14 @@ stream {
proxy_set_header Host {{ $location.ExternalAuth.Host }};
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_set_header X-Sent-From "nginx-ingress-controller";
{{ if $location.ExternalAuth.RequestRedirect }}
proxy_set_header X-Auth-Request-Redirect {{ $location.ExternalAuth.RequestRedirect }};
{{ else }}
proxy_set_header X-Auth-Request-Redirect $request_uri;
{{ end }}
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_pass_request_headers on;
@ -726,7 +729,7 @@ stream {
}
{{ end }}
{{ if not (empty $authPath) }}
{{ if $authPath }}
# this location requires authentication
auth_request {{ $authPath }};
auth_request_set $auth_cookie $upstream_http_set_cookie;
@ -736,7 +739,7 @@ stream {
{{- end }}
{{ end }}
{{ if not (empty $location.ExternalAuth.SigninURL) }}
{{ if $location.ExternalAuth.SigninURL }}
error_page 401 = {{ buildAuthSignURL $location.ExternalAuth.SigninURL }};
{{ end }}
@ -778,7 +781,6 @@ stream {
proxy_set_header Host $host;
{{ end }}
# Pass the extracted client certificate to the backend
{{ if not (empty $server.CertificateAuth.CAFileName) }}
{{ if $server.CertificateAuth.PassCertToUpstream }}
@ -861,7 +863,6 @@ stream {
proxy_set_header X-Service-Name $service_name;
{{ end }}
{{ if not (empty $location.Backend) }}
{{ buildProxyPass $server.Hostname $all.Backends $location }}
{{ if (or (eq $location.Proxy.ProxyRedirectFrom "default") (eq $location.Proxy.ProxyRedirectFrom "off")) }}