Merge pull request #4278 from moolen/feat/auth-req-cache

feat: auth-req caching
This commit is contained in:
Kubernetes Prow Robot 2019-07-17 12:06:12 -07:00 committed by GitHub
commit 589c9a20f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 583 additions and 52 deletions

View file

@ -26,6 +26,8 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/auth-tls-error-page](#client-certificate-authentication)|string|
|[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-cache-key](#external-authentication)|string|
|[nginx.ingress.kubernetes.io/auth-cache-duration](#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|
@ -379,6 +381,10 @@ Additionally it is possible to set:
`<Response_Header_1, ..., Response_Header_n>` to specify headers to pass to backend once authentication request completes.
* `nginx.ingress.kubernetes.io/auth-request-redirect`:
`<Request_Redirect_URL>` to specify the X-Auth-Request-Redirect header value.
* `nginx.ingress.kubernetes.io/auth-cache-key`:
`<Cache_Key>` this enables caching for auth requests. specify a lookup key for auth responses. e.g. `$remote_user$http_authorization`. Each server and location has it's own keyspace. Hence a cached response is only valid on a per-server and per-location basis.
* `nginx.ingress.kubernetes.io/auth-cache-duration`:
`<Cache_duration>` to specify a caching time for auth responses based on their response codes, e.g. `200 202 30m`. See [proxy_cache_valid](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid) for details. You may specify multiple, comma-separated values: `200 202 10m, 401 5m`. defaults to `200 202 401 5m`.
* `nginx.ingress.kubernetes.io/auth-snippet`:
`<Auth_Snippet>` to specify a custom snippet to use with external authentication, e.g.

View file

@ -160,6 +160,8 @@ The following table shows a configuration option's name, type, and the default v
|[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|""|
|[global-auth-cache-key](#global-auth-cache-key)|string|""|
|[global-auth-cache-duration](#global-auth-cache-duration)|string|"200 202 401 5m"|
|[no-auth-locations](#no-auth-locations)|string|"/.well-known/acme-challenge"|
|[block-cidrs](#block-cidrs)|[]string|""|
|[block-user-agents](#block-user-agents)|[]string|""|
@ -922,6 +924,14 @@ Sets a custom snippet to use with external authentication. Applied to all the lo
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-request-redirect`.
_**default:**_ ""
## global-auth-cache-key
Enables caching for global auth requests. Specify a lookup key for auth responses, e.g. `$remote_user$http_authorization`.
## global-auth-cache-duration
Set a caching time for auth responses based on their response codes, e.g. `200 202 30m`. See [proxy_cache_valid](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid) for details. You may specify multiple, comma-separated values: `200 202 10m, 401 5m`. defaults to `200 202 401 5m`.
## no-auth-locations
A comma-separated list of locations that should not get authenticated.

View file

@ -42,8 +42,13 @@ type Config struct {
ResponseHeaders []string `json:"responseHeaders,omitempty"`
RequestRedirect string `json:"requestRedirect"`
AuthSnippet string `json:"authSnippet"`
AuthCacheKey string `json:"authCacheKey"`
AuthCacheDuration []string `json:"authCacheDuration"`
}
// DefaultCacheDuration is the fallback value if no cache duration is provided
const DefaultCacheDuration = "200 202 401 5m"
// Equal tests for equality between two Config types
func (e1 *Config) Equal(e2 *Config) bool {
if e1 == e2 {
@ -77,12 +82,23 @@ func (e1 *Config) Equal(e2 *Config) bool {
return false
}
if e1.AuthCacheKey != e2.AuthCacheKey {
return false
}
match = sets.StringElementsMatch(e1.AuthCacheDuration, e2.AuthCacheDuration)
if !match {
return false
}
return true
}
var (
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`)
durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html
)
// ValidMethod checks is the provided string a valid HTTP method
@ -104,6 +120,31 @@ func ValidHeader(header string) bool {
return headerRegexp.Match([]byte(header))
}
// ValidCacheDuration checks if the provided string is a valid cache duration
// spec: [code ...] [time ...];
// with: code is an http status code
// time must match the time regex and may appear multiple times, e.g. `1h 30m`
func ValidCacheDuration(duration string) bool {
elements := strings.Split(duration, " ")
seenDuration := false
for _, element := range elements {
if len(element) == 0 {
continue
}
if statusCodeRegex.Match([]byte(element)) {
if seenDuration {
return false // code after duration
}
continue
}
if durationRegex.Match([]byte(element)) {
seenDuration = true
}
}
return seenDuration
}
type authReq struct {
r resolver.Resolver
}
@ -143,6 +184,17 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
klog.V(3).Infof("auth-snippet annotation is undefined and will not be set")
}
authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing)
if err != nil {
klog.V(3).Infof("auth-cache-key annotation is undefined and will not be set")
}
durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing)
authCacheDuration, err := ParseStringToCacheDurations(durstr)
if err != nil {
return nil, err
}
responseHeaders := []string{}
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
if len(hstr) != 0 {
@ -168,6 +220,8 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
ResponseHeaders: responseHeaders,
RequestRedirect: requestRedirect,
AuthSnippet: authSnippet,
AuthCacheKey: authCacheKey,
AuthCacheDuration: authCacheDuration,
}, nil
}
@ -189,3 +243,28 @@ func ParseStringToURL(input string) (*url.URL, string) {
return parsedURL, ""
}
// ParseStringToCacheDurations parses and validates the provided string
// into a list of cache durations.
// It will always return at least one duration (the default duration)
func ParseStringToCacheDurations(input string) ([]string, error) {
authCacheDuration := []string{}
if len(input) != 0 {
arr := strings.Split(input, ",")
for _, duration := range arr {
duration = strings.TrimSpace(duration)
if len(duration) > 0 {
if !ValidCacheDuration(duration) {
authCacheDuration = []string{DefaultCacheDuration}
return authCacheDuration, ing_errors.NewLocationDenied(fmt.Sprintf("invalid cache duration: %s", duration))
}
authCacheDuration = append(authCacheDuration, duration)
}
}
}
if len(authCacheDuration) == 0 {
authCacheDuration = append(authCacheDuration, DefaultCacheDuration)
}
return authCacheDuration, nil
}

View file

@ -79,17 +79,19 @@ func TestAnnotations(t *testing.T) {
method string
requestRedirect string
authSnippet string
authCacheKey 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},
{"valid URL - request redirect", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", "http://foo.com/redirect-me", "", false},
{"auth snippet", "http://foo.com/external-auth", "http://foo.com/external-auth", "", "", "proxy_set_header My-Custom-Header 42;", 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},
{"auth snippet", "http://foo.com/external-auth", "http://foo.com/external-auth", "", "", "proxy_set_header My-Custom-Header 42;", "", false},
{"auth cache ", "http://foo.com/external-auth", "http://foo.com/external-auth", "", "", "", "$foo$bar", false},
}
for _, test := range tests {
@ -98,6 +100,7 @@ func TestAnnotations(t *testing.T) {
data[parser.GetAnnotationWithPrefix("auth-method")] = fmt.Sprintf("%v", test.method)
data[parser.GetAnnotationWithPrefix("auth-request-redirect")] = test.requestRedirect
data[parser.GetAnnotationWithPrefix("auth-snippet")] = test.authSnippet
data[parser.GetAnnotationWithPrefix("auth-cache-key")] = test.authCacheKey
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if test.expErr {
@ -129,6 +132,9 @@ func TestAnnotations(t *testing.T) {
if u.AuthSnippet != test.authSnippet {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.authSnippet, u.AuthSnippet)
}
if u.AuthCacheKey != test.authCacheKey {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.authCacheKey, u.AuthCacheKey)
}
}
}
@ -180,6 +186,54 @@ func TestHeaderAnnotations(t *testing.T) {
}
}
func TestCacheDurationAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
ing.SetAnnotations(data)
tests := []struct {
title string
url string
duration string
parsedDuration []string
expErr bool
}{
{"nothing", "http://goog.url", "", []string{DefaultCacheDuration}, false},
{"spaces", "http://goog.url", " ", []string{DefaultCacheDuration}, false},
{"one duration", "http://goog.url", "5m", []string{"5m"}, false},
{"two durations", "http://goog.url", "200 202 10m, 401 5m", []string{"200 202 10m", "401 5m"}, false},
{"two durations and empty entries", "http://goog.url", ",5m,,401 10m,", []string{"5m", "401 10m"}, false},
{"only status code provided", "http://goog.url", "200", []string{DefaultCacheDuration}, true},
{"mixed valid/invalid", "http://goog.url", "5m, xaxax", []string{DefaultCacheDuration}, true},
{"code after duration", "http://goog.url", "5m 200", []string{DefaultCacheDuration}, true},
}
for _, test := range tests {
data[parser.GetAnnotationWithPrefix("auth-url")] = test.url
data[parser.GetAnnotationWithPrefix("auth-cache-duration")] = test.duration
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", err.Error())
}
continue
}
t.Log(i)
u, ok := i.(*Config)
if !ok {
t.Errorf("%v: expected an External type", test.title)
continue
}
if !reflect.DeepEqual(u.AuthCacheDuration, test.parsedDuration) {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.duration, u.AuthCacheDuration)
}
}
}
func TestParseStringToURL(t *testing.T) {
validURL := "http://bar.foo.com/external-auth"
validParsedURL, _ := url.Parse(validURL)
@ -214,3 +268,35 @@ func TestParseStringToURL(t *testing.T) {
}
}
func TestParseStringToCacheDurations(t *testing.T) {
tests := []struct {
title string
duration string
expectedDurations []string
expErr bool
}{
{"empty", "", []string{DefaultCacheDuration}, false},
{"invalid", ",200,", []string{DefaultCacheDuration}, true},
{"single", ",200 5m,", []string{"200 5m"}, false},
{"multiple with duration", ",5m,,401 10m,", []string{"5m", "401 10m"}, false},
{"multiple durations", "200 202 401 5m, 418 30m", []string{"200 202 401 5m", "418 30m"}, false},
}
for _, test := range tests {
dur, err := ParseStringToCacheDurations(test.duration)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but nil was returned", test.title)
}
continue
}
if !reflect.DeepEqual(dur, test.expectedDurations) {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.expectedDurations, dur)
}
}
}

View file

@ -622,7 +622,7 @@ func NewDefault() Configuration {
defNginxStatusIpv4Whitelist = append(defNginxStatusIpv4Whitelist, "127.0.0.1")
defNginxStatusIpv6Whitelist = append(defNginxStatusIpv6Whitelist, "::1")
defProxyDeadlineDuration := time.Duration(5) * time.Second
defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", append(defResponseHeaders, ""), "", ""}
defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", append(defResponseHeaders, ""), "", "", "", []string{}}
cfg := Configuration{
AllowBackendServerHeader: false,
@ -814,4 +814,6 @@ type GlobalExternalAuth struct {
ResponseHeaders []string `json:"responseHeaders,omitempty"`
RequestRedirect string `json:"requestRedirect"`
AuthSnippet string `json:"authSnippet"`
AuthCacheKey string `json:"authCacheKey"`
AuthCacheDuration []string `json:"authCacheDuration"`
}

View file

@ -57,6 +57,8 @@ const (
globalAuthResponseHeaders = "global-auth-response-headers"
globalAuthRequestRedirect = "global-auth-request-redirect"
globalAuthSnippet = "global-auth-snippet"
globalAuthCacheKey = "global-auth-cache-key"
globalAuthCacheDuration = "global-auth-cache-duration"
)
var (
@ -226,6 +228,23 @@ func ReadConfig(src map[string]string) config.Configuration {
to.GlobalExternalAuth.AuthSnippet = val
}
if val, ok := conf[globalAuthCacheKey]; ok {
delete(conf, globalAuthCacheKey)
to.GlobalExternalAuth.AuthCacheKey = val
}
// Verify that the configured global external authorization cache duration is valid
if val, ok := conf[globalAuthCacheDuration]; ok {
delete(conf, globalAuthCacheDuration)
cacheDurations, err := authreq.ParseStringToCacheDurations(val)
if err != nil {
klog.Warningf("Global auth location denied - %s", err)
}
to.GlobalExternalAuth.AuthCacheDuration = cacheDurations
}
// 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)

View file

@ -25,6 +25,7 @@ import (
"github.com/kylelemons/godebug/pretty"
"github.com/mitchellh/hashstructure"
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
"k8s.io/ingress-nginx/internal/ingress/controller/config"
)
@ -280,3 +281,25 @@ func TestGlobalExternalAuthSnippetParsing(t *testing.T) {
}
}
}
func TestGlobalExternalAuthCacheDurationParsing(t *testing.T) {
testCases := map[string]struct {
durations string
expect []string
}{
"nothing": {"", []string{authreq.DefaultCacheDuration}},
"spaces": {" ", []string{authreq.DefaultCacheDuration}},
"one duration": {"5m", []string{"5m"}},
"two durations and empty entries": {",200 5m,,401 30m,", []string{"200 5m", "401 30m"}},
"only status code provided": {"200", []string{authreq.DefaultCacheDuration}},
"mixed valid/invalid": {"5m, xaxax", []string{authreq.DefaultCacheDuration}},
}
for n, tc := range testCases {
cfg := ReadConfig(map[string]string{"global-auth-cache-duration": tc.durations})
if !reflect.DeepEqual(cfg.GlobalExternalAuth.AuthCacheDuration, tc.expect) {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.AuthCacheDuration)
}
}
}

View file

@ -443,6 +443,9 @@ http {
{{ $zone }}
{{ end }}
# Cache for internal auth checks
proxy_cache_path /tmp/nginx-cache-auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=30m use_temp_path=off;
# Global filters
{{ range $ip := $cfg.BlockCIDRs }}deny {{ trimSpace $ip }};
{{ end }}
@ -896,6 +899,23 @@ stream {
location = {{ $authPath }} {
internal;
{{ if $externalAuth.AuthCacheKey }}
set $tmp_cache_key '{{ $server.Hostname }}{{ $authPath }}{{ $externalAuth.AuthCacheKey }}';
set $cache_key '';
rewrite_by_lua_block {
ngx.var.cache_key = ngx.encode_base64(ngx.sha1_bin(ngx.var.tmp_cache_key))
}
proxy_cache auth_cache;
{{- range $dur := $externalAuth.AuthCacheDuration }}
proxy_cache_valid {{ $dur }};
{{- end }}
proxy_cache_key "$cache_key";
{{ end }}
# ngx_auth_request module overrides variables in the parent request,
# therefore we have to explicitly set this variable again so that when the parent request
# resumes it has the correct value set for this variable so that Lua can pick backend correctly
@ -928,7 +948,11 @@ stream {
proxy_set_header X-Auth-Request-Redirect $request_uri;
{{ end }}
{{ if $externalAuth.AuthCacheKey }}
proxy_buffering "on";
{{ else }}
proxy_buffering {{ $location.Proxy.ProxyBuffering }};
{{ end }}
proxy_buffer_size {{ $location.Proxy.BufferSize }};
proxy_buffers {{ $location.Proxy.BuffersNumber }} {{ $location.Proxy.BufferSize }};
proxy_request_buffering {{ $location.Proxy.RequestBuffering }};

View file

@ -260,6 +260,26 @@ var _ = framework.IngressNginxDescribe("Annotations - Auth", func() {
})
})
It(`should set cache_key when external auth cache is configured`, func() {
host := "auth"
annotations := map[string]string{
"nginx.ingress.kubernetes.io/auth-url": "http://foo.bar/basic-auth/user/password",
"nginx.ingress.kubernetes.io/auth-cache-key": "foo",
"nginx.ingress.kubernetes.io/auth-cache-duration": "200 202 401 30m",
}
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, "http-svc", 80, &annotations)
f.EnsureIngress(ing)
f.WaitForNginxServer(host,
func(server string) bool {
return Expect(server).Should(MatchRegexp(`\$cache_key.*foo`)) &&
Expect(server).Should(ContainSubstring(`proxy_cache_valid 200 202 401 30m;`))
})
})
Context("when external authentication is configured", func() {
host := "auth"
@ -322,6 +342,185 @@ var _ = framework.IngressNginxDescribe("Annotations - Auth", func() {
Expect(resp.Header.Get("Location")).Should(Equal(fmt.Sprintf("http://%s/auth/start?rd=http://%s%s", host, host, url.QueryEscape("/?a=b&c=d"))))
})
})
Context("when external authentication with caching is configured", func() {
thisHost := "auth"
thatHost := "different"
fooPath := "/foo"
barPath := "/bar"
BeforeEach(func() {
f.NewHttpbinDeployment()
var httpbinIP string
err := framework.WaitForEndpoints(f.KubeClientSet, framework.DefaultTimeout, "httpbin", f.Namespace, 1)
Expect(err).NotTo(HaveOccurred())
e, err := f.KubeClientSet.CoreV1().Endpoints(f.Namespace).Get("httpbin", metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
httpbinIP = e.Subsets[0].Addresses[0].IP
annotations := map[string]string{
"nginx.ingress.kubernetes.io/auth-url": fmt.Sprintf("http://%s/basic-auth/user/password", httpbinIP),
"nginx.ingress.kubernetes.io/auth-signin": "http://$host/auth/start",
"nginx.ingress.kubernetes.io/auth-cache-key": "fixed",
"nginx.ingress.kubernetes.io/auth-cache-duration": "200 201 401 30m",
}
for _, host := range []string{thisHost, thatHost} {
By("Adding an ingress rule for /foo")
fooIng := framework.NewSingleIngress(fmt.Sprintf("foo-%s-ing", host), fooPath, host, f.Namespace, "http-svc", 80, &annotations)
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(fmt.Sprintf("bar-%s-ing", host), barPath, host, f.Namespace, "http-svc", 80, &annotations)
f.EnsureIngress(barIng)
f.WaitForNginxServer(host, func(server string) bool {
return Expect(server).Should(ContainSubstring("location /bar"))
})
}
})
It("should return status code 200 when signed in after auth backend is deleted ", func() {
resp, _, errs := gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
err := f.DeleteDeployment("httpbin")
Expect(err).NotTo(HaveOccurred())
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
})
It("should deny login for different location on same server", func() {
resp, _, errs := gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
err := f.DeleteDeployment("httpbin")
Expect(err).NotTo(HaveOccurred())
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)+barPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
By("receiving an internal server error without cache on location /bar")
Expect(resp.StatusCode).Should(Equal(http.StatusInternalServerError))
})
It("should deny login for different servers", func() {
By("logging into server thisHost /foo")
resp, _, errs := gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
err := f.DeleteDeployment("httpbin")
Expect(err).NotTo(HaveOccurred())
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)+fooPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thatHost).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
By("receiving an internal server error without cache on thisHost location /bar")
Expect(resp.StatusCode).Should(Equal(http.StatusInternalServerError))
})
It("should redirect to signin url when not signed in", func() {
resp, _, errs := gorequest.New().
Get(f.GetURL(framework.HTTP)).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", thisHost).
RedirectPolicy(func(req gorequest.Request, via []gorequest.Request) error {
return http.ErrUseLastResponse
}).
Param("a", "b").
Param("c", "d").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusFound))
Expect(resp.Header.Get("Location")).Should(Equal(fmt.Sprintf("http://%s/auth/start?rd=http://%s%s", thisHost, thisHost, url.QueryEscape("/?a=b&c=d"))))
})
})
})
// TODO: test Digest Auth

View file

@ -17,6 +17,8 @@ limitations under the License.
package framework
import (
"time"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
@ -138,3 +140,14 @@ func (f *Framework) NewDeployment(name, image string, port int32, replicas int32
err = WaitForEndpoints(f.KubeClientSet, DefaultTimeout, name, f.Namespace, int(replicas))
Expect(err).NotTo(HaveOccurred(), "failed to wait for endpoints to become ready")
}
// DeleteDeployment deletes a deployment with a particular name and waits for the pods to be deleted
func (f *Framework) DeleteDeployment(name string) error {
d, err := f.KubeClientSet.AppsV1().Deployments(f.Namespace).Get(name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred(), "failed to get a deployment")
err = f.KubeClientSet.AppsV1().Deployments(f.Namespace).Delete(name, &metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred(), "failed to delete a deployment")
return WaitForPodsDeleted(f.KubeClientSet, time.Second*60, f.Namespace, metav1.ListOptions{
LabelSelector: labelSelectorToString(d.Spec.Selector.MatchLabels),
})
}

View file

@ -143,6 +143,21 @@ func WaitForPodsReady(kubeClientSet kubernetes.Interface, timeout time.Duration,
})
}
// WaitForPodsDeleted waits for a given amount of time until a group of Pods are deleted in the given namespace.
func WaitForPodsDeleted(kubeClientSet kubernetes.Interface, timeout time.Duration, namespace string, opts metav1.ListOptions) error {
return wait.Poll(2*time.Second, timeout, func() (bool, error) {
pl, err := kubeClientSet.CoreV1().Pods(namespace).List(opts)
if err != nil {
return false, nil
}
if len(pl.Items) == 0 {
return true, nil
}
return false, nil
})
}
// WaitForEndpoints waits for a given amount of time until an endpoint contains.
func WaitForEndpoints(kubeClientSet kubernetes.Interface, timeout time.Duration, name, ns string, expectedEndpoints int) error {
if expectedEndpoints == 0 {

View file

@ -288,6 +288,14 @@ func podRunning(c kubernetes.Interface, podName, namespace string) wait.Conditio
}
}
func labelSelectorToString(labels map[string]string) string {
var str string
for k, v := range labels {
str += fmt.Sprintf("%s=%s,", k, v)
}
return str[:len(str)-1]
}
// NewInt32 converts int32 to a pointer
func NewInt32(val int32) *int32 {
p := new(int32)

View file

@ -19,6 +19,7 @@ package settings
import (
"fmt"
"net/http"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@ -146,6 +147,52 @@ var _ = framework.IngressNginxDescribe("Global External Auth", func() {
Expect(barResp.StatusCode).Should(Equal(http.StatusOK))
})
It("should still return status code 200 after auth backend is deleted using cache ", func() {
globalExternalAuthCacheKeySetting := "global-auth-cache-key"
globalExternalAuthCacheKey := "foo"
globalExternalAuthCacheDurationSetting := "global-auth-cache-duration"
globalExternalAuthCacheDuration := "200 201 401 30m"
globalExternalAuthURL := fmt.Sprintf("http://httpbin.%s.svc.cluster.local:80/status/200", f.Namespace)
By("Adding a global-auth-cache-key to configMap")
f.UpdateNginxConfigMapData(globalExternalAuthCacheKeySetting, globalExternalAuthCacheKey)
f.UpdateNginxConfigMapData(globalExternalAuthCacheDurationSetting, globalExternalAuthCacheDuration)
f.UpdateNginxConfigMapData(globalExternalAuthURLSetting, globalExternalAuthURL)
f.WaitForNginxServer(host,
func(server string) bool {
return Expect(server).Should(MatchRegexp(`\$cache_key.*foo`)) &&
Expect(server).Should(ContainSubstring(`proxy_cache_valid 200 201 401 30m;`))
})
resp, _, errs := gorequest.New().
Get(f.GetURL(framework.HTTP)+barPath).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", host).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
err := f.DeleteDeployment("httpbin")
Expect(err).NotTo(HaveOccurred())
resp, _, errs = gorequest.New().
Get(f.GetURL(framework.HTTP)).
Retry(10, 1*time.Second, http.StatusNotFound).
Set("Host", host).
SetBasicAuth("user", "password").
End()
for _, err := range errs {
Expect(err).NotTo(HaveOccurred())
}
})
It(`should proxy_method method when global-auth-method is configured`, func() {
globalExternalAuthMethodSetting := "global-auth-method"