diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md old mode 100644 new mode 100755 index 0be636124f..830ab4f77f --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -27,6 +27,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz |[nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream](#client-certificate-authentication)|"true" or "false"| |[nginx.ingress.kubernetes.io/auth-url](#external-authentication)|string| |[nginx.ingress.kubernetes.io/auth-snippet](#external-authentication)|string| +|[nginx.ingress.kubernetes.io/enable-global-auth](#external-authentication)|"true" or "false"| |[nginx.ingress.kubernetes.io/backend-protocol](#backend-protocol)|string|HTTP,HTTPS,GRPC,GRPCS,AJP| |[nginx.ingress.kubernetes.io/canary](#canary)|"true" or "false"| |[nginx.ingress.kubernetes.io/canary-by-header](#canary)|string| @@ -389,6 +390,14 @@ nginx.ingress.kubernetes.io/auth-snippet: | !!! example Please check the [external-auth](../../examples/auth/external-auth/README.md) example. +#### Global External Authentication + +By default the controller redirects all requests to an existing service that provides authentication if `global-auth-url` is set in the NGINX ConfigMap. If you want to disable this behavior for that ingress, you can use `enable-global-auth: "false"` in the NGINX ConfigMap. +`nginx.ingress.kubernetes.io/enable-global-auth`: + indicates if GlobalExternalAuth configuration should be applied or not to this Ingress rule. Default values is set to `"true"`. + +!!! note For more information please see [global-auth-url](./configmap.md#global-auth-url). + ### Rate limiting These annotations define a limit on the connections that can be opened by a single client IP address. diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md old mode 100644 new mode 100755 index d13d49149c..37974a67c7 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -152,6 +152,12 @@ The following table shows a configuration option's name, type, and the default v |[limit-req-status-code](#limit-req-status-code)|int|503| |[limit-conn-status-code](#limit-conn-status-code)|int|503| |[no-tls-redirect-locations](#no-tls-redirect-locations)|string|"/.well-known/acme-challenge"| +|[global-auth-url](#global-auth-url)|string|""| +|[global-auth-method](#global-auth-method)|string|""| +|[global-auth-signin](#global-auth-signin)|string|""| +|[global-auth-response-headers](#global-auth-response-headers)|string|""| +|[global-auth-request-redirect](#global-auth-request-redirect)|string|""| +|[global-auth-snippet](#global-auth-snippet)|string|""| |[no-auth-locations](#no-auth-locations)|string|"/.well-known/acme-challenge"| |[block-cidrs](#block-cidrs)|[]string|""| |[block-user-agents](#block-user-agents)|[]string|""| @@ -864,6 +870,45 @@ Sets the [status code to return in response to rejected connections](http://ngin A comma-separated list of locations on which http requests will never get redirected to their https counterpart. _**default:**_ "/.well-known/acme-challenge" +## global-auth-url + +A url to an existing service that provides authentication for all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-url`. +Locations that should not get authenticated can be listed using `no-auth-locations` See [no-auth-locations](#no-auth-locations). In addition, each service can be excluded from authentication via annotation `enable-global-auth` set to "false". +_**default:**_ "" + +_References:_ [https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md#external-authentication](https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md#external-authentication) + +## global-auth-method + +A HTTP method to use for an existing service that provides authentication for all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-method`. +_**default:**_ "" + +## global-auth-signin + +Sets the location of the error page for an existing service that provides authentication for all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-signin`. +_**default:**_ "" + +## global-auth-response-headers + +Sets the headers to pass to backend once authentication request completes. Applied to all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-response-headers`. +_**default:**_ "" + +## global-auth-request-redirect + +Sets the X-Auth-Request-Redirect header value. Applied to all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-request-redirect`. +_**default:**_ "" + +## global-auth-snippet + +Sets a custom snippet to use with external authentication. Applied to all the locations. +Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-request-redirect`. +_**default:**_ "" + ## no-auth-locations A comma-separated list of locations that should not get authenticated. diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go old mode 100644 new mode 100755 index b803ebedc9..32ebe6c05c --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -30,6 +30,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/alias" "k8s.io/ingress-nginx/internal/ingress/annotations/auth" "k8s.io/ingress-nginx/internal/ingress/annotations/authreq" + "k8s.io/ingress-nginx/internal/ingress/annotations/authreqglobal" "k8s.io/ingress-nginx/internal/ingress/annotations/authtls" "k8s.io/ingress-nginx/internal/ingress/annotations/backendprotocol" "k8s.io/ingress-nginx/internal/ingress/annotations/clientbodybuffersize" @@ -83,6 +84,7 @@ type Ingress struct { //TODO: Change this back into an error when https://github.com/imdario/mergo/issues/100 is resolved Denied *string ExternalAuth authreq.Config + EnableGlobalAuth bool HTTP2PushPreload bool Proxy proxy.Config RateLimit ratelimit.Config @@ -127,6 +129,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "CustomHTTPErrors": customhttperrors.NewParser(cfg), "DefaultBackend": defaultbackend.NewParser(cfg), "ExternalAuth": authreq.NewParser(cfg), + "EnableGlobalAuth": authreqglobal.NewParser(cfg), "HTTP2PushPreload": http2pushpreload.NewParser(cfg), "Proxy": proxy.NewParser(cfg), "RateLimit": ratelimit.NewParser(cfg), diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go old mode 100644 new mode 100755 index 93666d1aaf..4721c8d5ee --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -17,6 +17,7 @@ limitations under the License. package authreq import ( + "fmt" "net/url" "regexp" "strings" @@ -84,7 +85,8 @@ var ( headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`) ) -func validMethod(method string) bool { +// ValidMethod checks is the provided string a valid HTTP method +func ValidMethod(method string) bool { if len(method) == 0 { return false } @@ -97,7 +99,8 @@ func validMethod(method string) bool { return false } -func validHeader(header string) bool { +// ValidHeader checks is the provided string satisfies the header's name regex +func ValidHeader(header string) bool { return headerRegexp.Match([]byte(header)) } @@ -119,22 +122,13 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { return nil, err } - authURL, err := url.Parse(urlString) - if err != nil { - return nil, err - } - if authURL.Scheme == "" { - return nil, ing_errors.NewLocationDenied("url scheme is empty") - } - if authURL.Host == "" { - return nil, ing_errors.NewLocationDenied("url host is empty") - } - if strings.Contains(authURL.Host, "..") { - return nil, ing_errors.NewLocationDenied("invalid url host") + authURL, message := ParseStringToURL(urlString) + if authURL == nil { + return nil, ing_errors.NewLocationDenied(message) } authMethod, _ := parser.GetStringAnnotation("auth-method", ing) - if len(authMethod) != 0 && !validMethod(authMethod) { + if len(authMethod) != 0 && !ValidMethod(authMethod) { return nil, ing_errors.NewLocationDenied("invalid HTTP method") } @@ -156,7 +150,7 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { for _, header := range harr { header = strings.TrimSpace(header) if len(header) > 0 { - if !validHeader(header) { + if !ValidHeader(header) { return nil, ing_errors.NewLocationDenied("invalid headers list") } responseHeaders = append(responseHeaders, header) @@ -176,3 +170,22 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { AuthSnippet: authSnippet, }, nil } + +// ParseStringToURL parses the provided string into URL and returns error +// message in case of failure +func ParseStringToURL(input string) (*url.URL, string) { + + parsedURL, err := url.Parse(input) + if err != nil { + return nil, fmt.Sprintf("%v is not a valid URL: %v", input, err) + } + if parsedURL.Scheme == "" { + return nil, "url scheme is empty." + } else if parsedURL.Host == "" { + return nil, "url host is empty." + } else if strings.Contains(parsedURL.Host, "..") { + return nil, "invalid url host." + } + return parsedURL, "" + +} diff --git a/internal/ingress/annotations/authreq/main_test.go b/internal/ingress/annotations/authreq/main_test.go old mode 100644 new mode 100755 index c9aebc6784..767ae53fc4 --- a/internal/ingress/annotations/authreq/main_test.go +++ b/internal/ingress/annotations/authreq/main_test.go @@ -18,6 +18,7 @@ package authreq import ( "fmt" + "net/url" "reflect" "testing" @@ -178,3 +179,38 @@ func TestHeaderAnnotations(t *testing.T) { } } } + +func TestParseStringToURL(t *testing.T) { + validURL := "http://bar.foo.com/external-auth" + validParsedURL, _ := url.Parse(validURL) + + tests := []struct { + title string + url string + message string + parsed *url.URL + expErr bool + }{ + {"empty", "", "url scheme is empty.", nil, true}, + {"no scheme", "bar", "url scheme is empty.", nil, true}, + {"invalid host", "http://", "url host is empty.", nil, true}, + {"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host.", nil, true}, + {"valid URL", validURL, "", validParsedURL, false}, + } + + for _, test := range tests { + + i, err := ParseStringToURL(test.url) + if test.expErr { + if err != test.message { + t.Errorf("%v: expected error \"%v\" but \"%v\" was returned", test.title, test.message, err) + } + continue + } + + if i.String() != test.parsed.String() { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.parsed, i) + } + } + +} diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go new file mode 100755 index 0000000000..e8c976f3cf --- /dev/null +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -0,0 +1,45 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authreqglobal + +import ( + extensions "k8s.io/api/extensions/v1beta1" + + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +type authReqGlobal struct { + r resolver.Resolver +} + +// NewParser creates a new authentication request annotation parser +func NewParser(r resolver.Resolver) parser.IngressAnnotation { + return authReqGlobal{r} +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to enable or disable global external authentication +func (a authReqGlobal) Parse(ing *extensions.Ingress) (interface{}, error) { + + enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing) + if err != nil { + enableGlobalAuth = true + } + + return enableGlobalAuth, nil +} diff --git a/internal/ingress/annotations/authreqglobal/main_test.go b/internal/ingress/annotations/authreqglobal/main_test.go new file mode 100755 index 0000000000..4f686c05a1 --- /dev/null +++ b/internal/ingress/annotations/authreqglobal/main_test.go @@ -0,0 +1,82 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authreqglobal + +import ( + "testing" + + api "k8s.io/api/core/v1" + extensions "k8s.io/api/extensions/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" + + "k8s.io/apimachinery/pkg/util/intstr" +) + +func buildIngress() *extensions.Ingress { + defaultBackend := extensions.IngressBackend{ + ServiceName: "default-backend", + ServicePort: intstr.FromInt(80), + } + + return &extensions.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + 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 TestAnnotation(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("auth-url")] = "http://foo.com/external-auth" + data[parser.GetAnnotationWithPrefix("enable-global-auth")] = "false" + ing.SetAnnotations(data) + + i, _ := NewParser(&resolver.Mock{}).Parse(ing) + u, ok := i.(bool) + if !ok { + t.Errorf("expected a Config type") + } + if u { + t.Errorf("Expected false but returned true") + } +} diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go old mode 100644 new mode 100755 index 961c862bba..f068564068 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -563,6 +563,11 @@ type Configuration struct { // should not get authenticated NoAuthLocations string `json:"no-auth-locations"` + // GlobalExternalAuth indicates the access to all locations requires + // authentication using an external provider + // +optional + GlobalExternalAuth GlobalExternalAuth `json:"global-external-auth"` + // DisableLuaRestyWAF disables lua-resty-waf globally regardless // of whether there's an ingress that has enabled the WAF using annotation DisableLuaRestyWAF bool `json:"disable-lua-resty-waf"` @@ -592,11 +597,13 @@ func NewDefault() Configuration { defBlockEntity := make([]string, 0) defNginxStatusIpv4Whitelist := make([]string, 0) defNginxStatusIpv6Whitelist := make([]string, 0) + defResponseHeaders := make([]string, 0) defIPCIDR = append(defIPCIDR, "0.0.0.0/0") defNginxStatusIpv4Whitelist = append(defNginxStatusIpv4Whitelist, "127.0.0.1") defNginxStatusIpv6Whitelist = append(defNginxStatusIpv6Whitelist, "::1") defProxyDeadlineDuration := time.Duration(5) * time.Second + defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", append(defResponseHeaders, ""), "", ""} cfg := Configuration{ AllowBackendServerHeader: false, @@ -715,6 +722,7 @@ func NewDefault() Configuration { SyslogPort: 514, NoTLSRedirectLocations: "/.well-known/acme-challenge", NoAuthLocations: "/.well-known/acme-challenge", + GlobalExternalAuth: defGlobalExternalAuth, } if klog.V(5) { @@ -772,3 +780,16 @@ type ListenPorts struct { Default int SSLProxy int } + +// GlobalExternalAuth describe external authentication configuration for the +// NGINX Ingress controller +type GlobalExternalAuth struct { + URL string `json:"url"` + // Host contains the hostname defined in the URL + Host string `json:"host"` + SigninURL string `json:"signinUrl"` + Method string `json:"method"` + ResponseHeaders []string `json:"responseHeaders,omitempty"` + RequestRedirect string `json:"requestRedirect"` + AuthSnippet string `json:"authSnippet"` +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go old mode 100644 new mode 100755 index f0fc95d839..4af63c17f1 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -98,6 +98,8 @@ type Configuration struct { ValidationWebhook string ValidationWebhookCertPath string ValidationWebhookKeyPath string + + GlobalExternalAuth *ngx_config.GlobalExternalAuth } // GetPublishService returns the Service used to set the load-balancer status of Ingresses. @@ -1151,6 +1153,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress) loc.ConfigurationSnippet = anns.ConfigurationSnippet loc.CorsConfig = anns.CorsConfig loc.ExternalAuth = anns.ExternalAuth + loc.EnableGlobalAuth = anns.EnableGlobalAuth loc.HTTP2PushPreload = anns.HTTP2PushPreload loc.Proxy = anns.Proxy loc.RateLimit = anns.RateLimit diff --git a/internal/ingress/controller/nginx.go b/internal/ingress/controller/nginx.go old mode 100644 new mode 100755 diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go old mode 100644 new mode 100755 diff --git a/internal/ingress/controller/store/store_test.go b/internal/ingress/controller/store/store_test.go old mode 100644 new mode 100755 diff --git a/internal/ingress/controller/template/configmap.go b/internal/ingress/controller/template/configmap.go old mode 100644 new mode 100755 index 0d09548497..6eaafc42cc --- a/internal/ingress/controller/template/configmap.go +++ b/internal/ingress/controller/template/configmap.go @@ -29,27 +29,34 @@ import ( "github.com/mitchellh/mapstructure" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/ingress-nginx/internal/ingress/annotations/authreq" "k8s.io/ingress-nginx/internal/ingress/controller/config" ing_net "k8s.io/ingress-nginx/internal/net" "k8s.io/ingress-nginx/internal/runtime" ) const ( - customHTTPErrors = "custom-http-errors" - skipAccessLogUrls = "skip-access-log-urls" - whitelistSourceRange = "whitelist-source-range" - proxyRealIPCIDR = "proxy-real-ip-cidr" - bindAddress = "bind-address" - httpRedirectCode = "http-redirect-code" - blockCIDRs = "block-cidrs" - blockUserAgents = "block-user-agents" - blockReferers = "block-referers" - proxyStreamResponses = "proxy-stream-responses" - hideHeaders = "hide-headers" - nginxStatusIpv4Whitelist = "nginx-status-ipv4-whitelist" - nginxStatusIpv6Whitelist = "nginx-status-ipv6-whitelist" - proxyHeaderTimeout = "proxy-protocol-header-timeout" - workerProcesses = "worker-processes" + customHTTPErrors = "custom-http-errors" + skipAccessLogUrls = "skip-access-log-urls" + whitelistSourceRange = "whitelist-source-range" + proxyRealIPCIDR = "proxy-real-ip-cidr" + bindAddress = "bind-address" + httpRedirectCode = "http-redirect-code" + blockCIDRs = "block-cidrs" + blockUserAgents = "block-user-agents" + blockReferers = "block-referers" + proxyStreamResponses = "proxy-stream-responses" + hideHeaders = "hide-headers" + nginxStatusIpv4Whitelist = "nginx-status-ipv4-whitelist" + nginxStatusIpv6Whitelist = "nginx-status-ipv6-whitelist" + proxyHeaderTimeout = "proxy-protocol-header-timeout" + workerProcesses = "worker-processes" + globalAuthURL = "global-auth-url" + globalAuthMethod = "global-auth-method" + globalAuthSignin = "global-auth-signin" + globalAuthResponseHeaders = "global-auth-response-headers" + globalAuthRequestRedirect = "global-auth-request-redirect" + globalAuthSnippet = "global-auth-snippet" ) var ( @@ -77,6 +84,7 @@ func ReadConfig(src map[string]string) config.Configuration { blockCIDRList := make([]string, 0) blockUserAgentList := make([]string, 0) blockRefererList := make([]string, 0) + responseHeaders := make([]string, 0) if val, ok := conf[customHTTPErrors]; ok { delete(conf, customHTTPErrors) @@ -150,6 +158,74 @@ func ReadConfig(src map[string]string) config.Configuration { } } + // Verify that the configured global external authorization URL is parsable as URL. if not, set the default value + if val, ok := conf[globalAuthURL]; ok { + delete(conf, globalAuthURL) + + authURL, message := authreq.ParseStringToURL(val) + if authURL == nil { + klog.Warningf("Global auth location denied - %v.", message) + } else { + to.GlobalExternalAuth.URL = val + to.GlobalExternalAuth.Host = authURL.Hostname() + } + } + + // Verify that the configured global external authorization method is a valid HTTP method. if not, set the default value + if val, ok := conf[globalAuthMethod]; ok { + delete(conf, globalAuthMethod) + + if len(val) != 0 && !authreq.ValidMethod(val) { + klog.Warningf("Global auth location denied - %v.", "invalid HTTP method") + } else { + to.GlobalExternalAuth.Method = val + } + } + + // Verify that the configured global external authorization error page is set and valid. if not, set the default value + if val, ok := conf[globalAuthSignin]; ok { + delete(conf, globalAuthSignin) + + signinURL, _ := authreq.ParseStringToURL(val) + if signinURL == nil { + klog.Warningf("Global auth location denied - %v.", "global-auth-signin setting is undefined and will not be set") + } else { + to.GlobalExternalAuth.SigninURL = val + } + } + + // Verify that the configured global external authorization response headers are valid. if not, set the default value + if val, ok := conf[globalAuthResponseHeaders]; ok { + delete(conf, globalAuthResponseHeaders) + + if len(val) != 0 { + harr := strings.Split(val, ",") + for _, header := range harr { + header = strings.TrimSpace(header) + if len(header) > 0 { + if !authreq.ValidHeader(header) { + klog.Warningf("Global auth location denied - %v.", "invalid headers list") + } else { + responseHeaders = append(responseHeaders, header) + } + } + } + } + to.GlobalExternalAuth.ResponseHeaders = responseHeaders + } + + if val, ok := conf[globalAuthRequestRedirect]; ok { + delete(conf, globalAuthRequestRedirect) + + to.GlobalExternalAuth.RequestRedirect = val + } + + if val, ok := conf[globalAuthSnippet]; ok { + delete(conf, globalAuthSnippet) + + to.GlobalExternalAuth.AuthSnippet = val + } + // Verify that the configured timeout is parsable as a duration. if not, set the default value if val, ok := conf[proxyHeaderTimeout]; ok { delete(conf, proxyHeaderTimeout) diff --git a/internal/ingress/controller/template/configmap_test.go b/internal/ingress/controller/template/configmap_test.go old mode 100644 new mode 100755 index 328169bdcf..0c1bc8e584 --- a/internal/ingress/controller/template/configmap_test.go +++ b/internal/ingress/controller/template/configmap_test.go @@ -153,3 +153,121 @@ func TestMergeConfigMapToStruct(t *testing.T) { t.Errorf("unexpected diff: (-got +want)\n%s", diff) } } + +func TestGlobalExternalAuthURLParsing(t *testing.T) { + errorURL := "" + validURL := "http://bar.foo.com/external-auth" + + testCases := map[string]struct { + url string + expect string + }{ + "no scheme": {"bar", errorURL}, + "invalid host": {"http://", errorURL}, + "invalid host (multiple dots)": {"http://foo..bar.com", errorURL}, + "valid URL": {validURL, validURL}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-url": tc.url}) + if cfg.GlobalExternalAuth.URL != tc.expect { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.URL) + } + } +} + +func TestGlobalExternalAuthMethodParsing(t *testing.T) { + testCases := map[string]struct { + method string + expect string + }{ + "invalid method": {"FOO", ""}, + "valid method": {"POST", "POST"}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-method": tc.method}) + if cfg.GlobalExternalAuth.Method != tc.expect { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.Method) + } + } +} + +func TestGlobalExternalAuthSigninParsing(t *testing.T) { + errorURL := "" + validURL := "http://bar.foo.com/auth-error-page" + + testCases := map[string]struct { + signin string + expect string + }{ + "no scheme": {"bar", errorURL}, + "invalid host": {"http://", errorURL}, + "invalid host (multiple dots)": {"http://foo..bar.com", errorURL}, + "valid URL": {validURL, validURL}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-signin": tc.signin}) + if cfg.GlobalExternalAuth.SigninURL != tc.expect { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.SigninURL) + } + } +} + +func TestGlobalExternalAuthResponseHeadersParsing(t *testing.T) { + testCases := map[string]struct { + headers string + expect []string + }{ + "single header": {"h1", []string{"h1"}}, + "nothing": {"", []string{}}, + "spaces": {" ", []string{}}, + "two headers": {"1,2", []string{"1", "2"}}, + "two headers and empty entries": {",1,,2,", []string{"1", "2"}}, + "header with spaces": {"1 2", []string{}}, + "header with other bad symbols": {"1+2", []string{}}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-response-headers": tc.headers}) + + if !reflect.DeepEqual(cfg.GlobalExternalAuth.ResponseHeaders, tc.expect) { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.ResponseHeaders) + } + } +} + +func TestGlobalExternalAuthRequestRedirectParsing(t *testing.T) { + testCases := map[string]struct { + requestRedirect string + expect string + }{ + "empty": {"", ""}, + "valid request redirect": {"http://foo.com/redirect-me", "http://foo.com/redirect-me"}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-request-redirect": tc.requestRedirect}) + if cfg.GlobalExternalAuth.RequestRedirect != tc.expect { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.RequestRedirect) + } + } +} + +func TestGlobalExternalAuthSnippetParsing(t *testing.T) { + testCases := map[string]struct { + authSnippet string + expect string + }{ + "empty": {"", ""}, + "auth snippet": {"proxy_set_header My-Custom-Header 42;", "proxy_set_header My-Custom-Header 42;"}, + } + + for n, tc := range testCases { + cfg := ReadConfig(map[string]string{"global-auth-snippet": tc.authSnippet}) + if cfg.GlobalExternalAuth.AuthSnippet != tc.expect { + t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.AuthSnippet) + } + } +} diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go old mode 100644 new mode 100755 index 2183715c15..ae750d2e6d --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -131,6 +131,7 @@ var ( "buildLuaSharedDictionaries": buildLuaSharedDictionaries, "buildLocation": buildLocation, "buildAuthLocation": buildAuthLocation, + "shouldApplyGlobalAuth": shouldApplyGlobalAuth, "buildAuthResponseHeaders": buildAuthResponseHeaders, "buildProxyPass": buildProxyPass, "filterRateLimits": filterRateLimits, @@ -397,14 +398,14 @@ func buildLocation(input interface{}, enforceRegex bool) string { return path } -func buildAuthLocation(input interface{}) string { +func buildAuthLocation(input interface{}, globalExternalAuthURL string) string { location, ok := input.(*ingress.Location) if !ok { klog.Errorf("expected an '*ingress.Location' type but %T was returned", input) return "" } - if location.ExternalAuth.URL == "" { + if (location.ExternalAuth.URL == "") && (!shouldApplyGlobalAuth(input, globalExternalAuthURL)) { return "" } @@ -414,19 +415,29 @@ func buildAuthLocation(input interface{}) string { return fmt.Sprintf("/_external-auth-%v", str) } -func buildAuthResponseHeaders(input interface{}) []string { +// shouldApplyGlobalAuth returns true only in case when ExternalAuth.URL is not set and +// GlobalExternalAuth is set and enabled +func shouldApplyGlobalAuth(input interface{}, globalExternalAuthURL string) bool { location, ok := input.(*ingress.Location) - res := []string{} if !ok { klog.Errorf("expected an '*ingress.Location' type but %T was returned", input) - return res } - if len(location.ExternalAuth.ResponseHeaders) == 0 { + if (location.ExternalAuth.URL == "") && (globalExternalAuthURL != "") && (location.EnableGlobalAuth) { + return true + } + + return false +} + +func buildAuthResponseHeaders(headers []string) []string { + res := []string{} + + if len(headers) == 0 { return res } - for i, h := range location.ExternalAuth.ResponseHeaders { + for i, h := range headers { hvar := strings.ToLower(h) hvar = strings.NewReplacer("-", "_").Replace(hvar) res = append(res, fmt.Sprintf("auth_request_set $authHeader%v $upstream_http_%v;", i, hvar)) diff --git a/internal/ingress/controller/template/template_test.go b/internal/ingress/controller/template/template_test.go old mode 100644 new mode 100755 index e5fa6c5c11..5ddc0eef1a --- a/internal/ingress/controller/template/template_test.go +++ b/internal/ingress/controller/template/template_test.go @@ -283,51 +283,106 @@ func TestBuildProxyPass(t *testing.T) { func TestBuildAuthLocation(t *testing.T) { invalidType := &ingress.Ingress{} expected := "" - actual := buildAuthLocation(invalidType) + actual := buildAuthLocation(invalidType, "") if !reflect.DeepEqual(expected, actual) { t.Errorf("Expected '%v' but returned '%v'", expected, actual) } authURL := "foo.com/auth" + globalAuthURL := "foo.com/global-auth" loc := &ingress.Location{ ExternalAuth: authreq.Config{ URL: authURL, }, - Path: "/cat", + Path: "/cat", + EnableGlobalAuth: true, } - str := buildAuthLocation(loc) - encodedAuthURL := strings.Replace(base64.URLEncoding.EncodeToString([]byte(loc.Path)), "=", "", -1) - expected = fmt.Sprintf("/_external-auth-%v", encodedAuthURL) + externalAuthPath := fmt.Sprintf("/_external-auth-%v", encodedAuthURL) - if str != expected { - t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, str) + testCases := []struct { + title string + authURL string + globalAuthURL string + enableglobalExternalAuth bool + expected string + }{ + {"authURL, globalAuthURL and enabled", authURL, globalAuthURL, true, externalAuthPath}, + {"authURL, globalAuthURL and disabled", authURL, globalAuthURL, false, externalAuthPath}, + {"authURL, empty globalAuthURL and enabled", authURL, "", true, externalAuthPath}, + {"authURL, empty globalAuthURL and disabled", authURL, "", false, externalAuthPath}, + {"globalAuthURL and enabled", "", globalAuthURL, true, externalAuthPath}, + {"globalAuthURL and disabled", "", globalAuthURL, false, ""}, + {"all empty and enabled", "", "", true, ""}, + {"all empty and disabled", "", "", false, ""}, } -} -func TestBuildAuthResponseHeaders(t *testing.T) { - invalidType := &ingress.Ingress{} - expected := []string{} - actual := buildAuthResponseHeaders(invalidType) + for _, testCase := range testCases { + loc.ExternalAuth.URL = testCase.authURL + loc.EnableGlobalAuth = testCase.enableglobalExternalAuth - if !reflect.DeepEqual(expected, actual) { - t.Errorf("Expected '%v' but returned '%v'", expected, actual) + str := buildAuthLocation(loc, testCase.globalAuthURL) + if str != testCase.expected { + t.Errorf("%v: expected '%v' but returned '%v'", testCase.title, testCase.expected, str) + } } +} + +func TestShouldApplyGlobalAuth(t *testing.T) { + + authURL := "foo.com/auth" + globalAuthURL := "foo.com/global-auth" loc := &ingress.Location{ - ExternalAuth: authreq.Config{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}}, + ExternalAuth: authreq.Config{ + URL: authURL, + }, + Path: "/cat", + EnableGlobalAuth: true, + } + + testCases := []struct { + title string + authURL string + globalAuthURL string + enableglobalExternalAuth bool + expected bool + }{ + {"authURL, globalAuthURL and enabled", authURL, globalAuthURL, true, false}, + {"authURL, globalAuthURL and disabled", authURL, globalAuthURL, false, false}, + {"authURL, empty globalAuthURL and enabled", authURL, "", true, false}, + {"authURL, empty globalAuthURL and disabled", authURL, "", false, false}, + {"globalAuthURL and enabled", "", globalAuthURL, true, true}, + {"globalAuthURL and disabled", "", globalAuthURL, false, false}, + {"all empty and enabled", "", "", true, false}, + {"all empty and disabled", "", "", false, false}, + } + + for _, testCase := range testCases { + loc.ExternalAuth.URL = testCase.authURL + loc.EnableGlobalAuth = testCase.enableglobalExternalAuth + + result := shouldApplyGlobalAuth(loc, testCase.globalAuthURL) + if result != testCase.expected { + t.Errorf("%v: expected '%v' but returned '%v'", testCase.title, testCase.expected, result) + } } - headers := buildAuthResponseHeaders(loc) - expected = []string{ +} + +func TestBuildAuthResponseHeaders(t *testing.T) { + externalAuthResponseHeaders := []string{"h1", "H-With-Caps-And-Dashes"} + 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;", } + headers := buildAuthResponseHeaders(externalAuthResponseHeaders) + if !reflect.DeepEqual(expected, headers) { t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, headers) } diff --git a/internal/ingress/types.go b/internal/ingress/types.go old mode 100644 new mode 100755 index 5483914f28..54afa9a180 --- a/internal/ingress/types.go +++ b/internal/ingress/types.go @@ -246,6 +246,9 @@ type Location struct { // authentication using an external provider // +optional ExternalAuth authreq.Config `json:"externalAuth,omitempty"` + // EnableGlobalAuth indicates if the access to this location requires + // authentication using an external provider defined in controller's config + EnableGlobalAuth bool `json:"enableGlobalAuth"` // HTTP2PushPreload allows to configure the HTTP2 Push Preload from backend // original location. // +optional diff --git a/internal/ingress/types_equals.go b/internal/ingress/types_equals.go old mode 100644 new mode 100755 index f838bb0aa0..d3ec2b2aba --- a/internal/ingress/types_equals.go +++ b/internal/ingress/types_equals.go @@ -352,6 +352,9 @@ func (l1 *Location) Equal(l2 *Location) bool { if !(&l1.ExternalAuth).Equal(&l2.ExternalAuth) { return false } + if l1.EnableGlobalAuth != l2.EnableGlobalAuth { + return false + } if l1.HTTP2PushPreload != l2.HTTP2PushPreload { return false } diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl old mode 100644 new mode 100755 index 2f7b66e4b3..4125d7ae4a --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -894,7 +894,13 @@ stream { {{ range $location := $server.Locations }} {{ $path := buildLocation $location $enforceRegex }} {{ $proxySetHeader := proxySetHeader $location }} - {{ $authPath := buildAuthLocation $location }} + {{ $authPath := buildAuthLocation $location $all.Cfg.GlobalExternalAuth.URL }} + {{ $applyGlobalAuth := shouldApplyGlobalAuth $location $all.Cfg.GlobalExternalAuth.URL }} + + {{ $externalAuth := $location.ExternalAuth }} + {{ if eq $applyGlobalAuth true }} + {{ $externalAuth = $all.Cfg.GlobalExternalAuth }} + {{ end }} {{ if not (empty $location.Rewrite.AppRoot)}} if ($uri = /) { @@ -915,13 +921,13 @@ stream { proxy_set_header Content-Length ""; proxy_set_header X-Forwarded-Proto ""; - {{ if $location.ExternalAuth.Method }} - proxy_method {{ $location.ExternalAuth.Method }}; + {{ if $externalAuth.Method }} + proxy_method {{ $externalAuth.Method }}; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Scheme $pass_access_scheme; {{ end }} - proxy_set_header Host {{ $location.ExternalAuth.Host }}; + proxy_set_header Host {{ $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-Sent-From "nginx-ingress-controller"; @@ -932,8 +938,8 @@ stream { proxy_set_header X-Forwarded-For $the_real_ip; {{ end }} - {{ if $location.ExternalAuth.RequestRedirect }} - proxy_set_header X-Auth-Request-Redirect {{ $location.ExternalAuth.RequestRedirect }}; + {{ if $externalAuth.RequestRedirect }} + proxy_set_header X-Auth-Request-Redirect {{ $externalAuth.RequestRedirect }}; {{ else }} proxy_set_header X-Auth-Request-Redirect $request_uri; {{ end }} @@ -963,15 +969,16 @@ stream { proxy_set_header ssl-client-issuer-dn $ssl_client_i_dn; {{ end }} - {{ if not (empty $location.ExternalAuth.AuthSnippet) }} - {{ $location.ExternalAuth.AuthSnippet }} + {{ if not (empty $externalAuth.AuthSnippet) }} + {{ $externalAuth.AuthSnippet }} {{ end }} - set $target {{ $location.ExternalAuth.URL }}; + set $target {{ $externalAuth.URL }}; proxy_pass $target; } {{ end }} + location {{ $path }} { {{ $ing := (getIngressInformation $location.Ingress $server.Hostname $location.Path) }} set $namespace "{{ $ing.Namespace }}"; @@ -1125,14 +1132,14 @@ stream { auth_request {{ $authPath }}; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; - {{- range $line := buildAuthResponseHeaders $location }} + {{- range $line := buildAuthResponseHeaders $externalAuth.ResponseHeaders }} {{ $line }} {{- end }} {{ end }} - {{ if $location.ExternalAuth.SigninURL }} + {{ if $externalAuth.SigninURL }} set_escape_uri $escaped_request_uri $request_uri; - error_page 401 = {{ buildAuthSignURL $location.ExternalAuth.SigninURL }}; + error_page 401 = {{ buildAuthSignURL $externalAuth.SigninURL }}; {{ end }} {{ if $location.BasicDigestAuth.Secured }} diff --git a/test/e2e/settings/global_external_auth.go b/test/e2e/settings/global_external_auth.go new file mode 100755 index 0000000000..c3916e869c --- /dev/null +++ b/test/e2e/settings/global_external_auth.go @@ -0,0 +1,217 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +import ( + "fmt" + "net/http" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/parnurzeal/gorequest" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +var _ = framework.IngressNginxDescribe("Global External Auth", func() { + f := framework.NewDefaultFramework("global-external-auth") + + host := "global-external-auth" + + echoServiceName := "http-svc" + + globalExternalAuthURLSetting := "global-auth-url" + + fooPath := "/foo" + barPath := "/bar" + + noAuthSetting := "no-auth-locations" + noAuthLocations := barPath + + enableGlobalExternalAuthAnnotation := "nginx.ingress.kubernetes.io/enable-global-auth" + + BeforeEach(func() { + f.NewEchoDeployment() + f.NewHttpbinDeployment() + }) + + AfterEach(func() { + }) + + Context("when global external authentication is configured", func() { + + BeforeEach(func() { + globalExternalAuthURL := fmt.Sprintf("http://httpbin.%s.svc.cluster.local:80/status/401", f.Namespace) + + By("Adding an ingress rule for /foo") + fooIng := framework.NewSingleIngress("foo-ingress", fooPath, host, f.Namespace, echoServiceName, 80, nil) + f.EnsureIngress(fooIng) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("location /foo")) + }) + + By("Adding an ingress rule for /bar") + barIng := framework.NewSingleIngress("bar-ingress", barPath, host, f.Namespace, echoServiceName, 80, nil) + f.EnsureIngress(barIng) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("location /bar")) + }) + + By("Adding a global-auth-url to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthURLSetting, globalExternalAuthURL) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring(globalExternalAuthURL)) + }) + }) + + It("should return status code 401 when request any protected service", func() { + + By("Sending a request to protected service /foo") + fooResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+fooPath). + Set("Host", host). + End() + Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized)) + + By("Sending a request to protected service /bar") + barResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+barPath). + Set("Host", host). + End() + Expect(barResp.StatusCode).Should(Equal(http.StatusUnauthorized)) + }) + + It("should return status code 200 when request whitelisted (via no-auth-locations) service and 401 when request protected service", func() { + + By("Adding a no-auth-locations for /bar to configMap") + f.UpdateNginxConfigMapData(noAuthSetting, noAuthLocations) + + By("Sending a request to protected service /foo") + fooResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+fooPath). + Set("Host", host). + End() + Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized)) + + By("Sending a request to whitelisted service /bar") + barResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+barPath). + Set("Host", host). + End() + Expect(barResp.StatusCode).Should(Equal(http.StatusOK)) + }) + + It("should return status code 200 when request whitelisted (via ingress annotation) service and 401 when request protected service", func() { + + By("Adding an ingress rule for /bar with annotation enable-global-auth = false") + annotations := map[string]string{ + enableGlobalExternalAuthAnnotation: "false", + } + barIng := framework.NewSingleIngress("bar-ingress", barPath, host, f.Namespace, echoServiceName, 80, &annotations) + f.EnsureIngress(barIng) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("location /bar")) + }) + + By("Sending a request to protected service /foo") + fooResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+fooPath). + Set("Host", host). + End() + Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized)) + + By("Sending a request to whitelisted service /bar") + barResp, _, _ := gorequest.New(). + Get(f.GetURL(framework.HTTP)+barPath). + Set("Host", host). + End() + Expect(barResp.StatusCode).Should(Equal(http.StatusOK)) + }) + + It(`should proxy_method method when global-auth-method is configured`, func() { + + globalExternalAuthMethodSetting := "global-auth-method" + globalExternalAuthMethod := "GET" + + By("Adding a global-auth-method to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthMethodSetting, globalExternalAuthMethod) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("proxy_method")) + }) + }) + + It(`should add custom error page when global-auth-signin url is configured`, func() { + + globalExternalAuthSigninSetting := "global-auth-signin" + globalExternalAuthSignin := "http://foo.com/global-error-page" + + By("Adding a global-auth-signin to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthSigninSetting, globalExternalAuthSignin) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("error_page 401 = ")) + }) + }) + + It(`should add auth headers when global-auth-response-headers is configured`, func() { + + globalExternalAuthResponseHeadersSetting := "global-auth-response-headers" + globalExternalAuthResponseHeaders := "Foo, Bar" + + By("Adding a global-auth-response-headers to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthResponseHeadersSetting, globalExternalAuthResponseHeaders) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring("auth_request_set $authHeader0 $upstream_http_foo;")) && + Expect(server).Should(ContainSubstring("auth_request_set $authHeader1 $upstream_http_bar;")) + }) + }) + + It(`should set request-redirect when global-auth-request-redirect is configured`, func() { + + globalExternalAuthRequestRedirectSetting := "global-auth-request-redirect" + globalExternalAuthRequestRedirect := "Foo-Redirect" + + By("Adding a global-auth-request-redirect to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthRequestRedirectSetting, globalExternalAuthRequestRedirect) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring(globalExternalAuthRequestRedirect)) + }) + }) + + It(`should set snippet when global external auth is configured`, func() { + + globalExternalAuthSnippetSetting := "global-auth-snippet" + globalExternalAuthSnippet := "proxy_set_header My-Custom-Header 42;" + + By("Adding a global-auth-snippet to configMap") + f.UpdateNginxConfigMapData(globalExternalAuthSnippetSetting, globalExternalAuthSnippet) + f.WaitForNginxServer(host, + func(server string) bool { + return Expect(server).Should(ContainSubstring(globalExternalAuthSnippet)) + }) + }) + + }) + +})