Merge pull request #4278 from moolen/feat/auth-req-cache
feat: auth-req caching
This commit is contained in:
commit
589c9a20f9
13 changed files with 583 additions and 52 deletions
|
|
@ -36,14 +36,19 @@ import (
|
|||
type Config 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"`
|
||||
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"`
|
||||
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\-_]+$`)
|
||||
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 {
|
||||
|
|
@ -161,13 +213,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
|
||||
|
||||
return &Config{
|
||||
URL: urlString,
|
||||
Host: authURL.Hostname(),
|
||||
SigninURL: signIn,
|
||||
Method: authMethod,
|
||||
ResponseHeaders: responseHeaders,
|
||||
RequestRedirect: requestRedirect,
|
||||
AuthSnippet: authSnippet,
|
||||
URL: urlString,
|
||||
Host: authURL.Hostname(),
|
||||
SigninURL: signIn,
|
||||
Method: authMethod,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -808,10 +808,12 @@ type ListenPorts struct {
|
|||
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"`
|
||||
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"`
|
||||
AuthCacheKey string `json:"authCacheKey"`
|
||||
AuthCacheDuration []string `json:"authCacheDuration"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue