Add a configurable URL redirect parameter for error URLs

This commit is contained in:
Ian Buss 2020-08-18 10:03:38 +01:00
parent 3d1acf6db0
commit 41cf628bdf
11 changed files with 233 additions and 86 deletions

View file

@ -35,15 +35,16 @@ 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"`
AuthCacheKey string `json:"authCacheKey"`
AuthCacheDuration []string `json:"authCacheDuration"`
ProxySetHeaders map[string]string `json:"proxySetHeaders,omitempty"`
Host string `json:"host"`
SigninURL string `json:"signinUrl"`
SigninURLRedirectParam string `json:"signinUrlRedirectParam,omitempty"`
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"`
ProxySetHeaders map[string]string `json:"proxySetHeaders,omitempty"`
}
// DefaultCacheDuration is the fallback value if no cache duration is provided
@ -66,6 +67,9 @@ func (e1 *Config) Equal(e2 *Config) bool {
if e1.SigninURL != e2.SigninURL {
return false
}
if e1.SigninURLRedirectParam != e2.SigninURLRedirectParam {
return false
}
if e1.Method != e2.Method {
return false
}
@ -174,6 +178,11 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
klog.V(3).InfoS("auth-signin annotation is undefined and will not be set")
}
signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing)
if err != nil {
klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set")
}
authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing)
if err != nil {
klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set")
@ -230,16 +239,17 @@ 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,
AuthCacheKey: authCacheKey,
AuthCacheDuration: authCacheDuration,
ProxySetHeaders: proxySetHeaders,
URL: urlString,
Host: authURL.Hostname(),
SigninURL: signIn,
SigninURLRedirectParam: signInRedirectParam,
Method: authMethod,
ResponseHeaders: responseHeaders,
RequestRedirect: requestRedirect,
AuthSnippet: authSnippet,
AuthCacheKey: authCacheKey,
AuthCacheDuration: authCacheDuration,
ProxySetHeaders: proxySetHeaders,
}, nil
}

View file

@ -72,30 +72,33 @@ func TestAnnotations(t *testing.T) {
ing.SetAnnotations(data)
tests := []struct {
title string
url string
signinURL string
method string
requestRedirect string
authSnippet string
authCacheKey string
expErr bool
title string
url string
signinURL string
signinURLRedirectParam string
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},
{"auth cache ", "http://foo.com/external-auth", "http://foo.com/external-auth", "", "", "", "$foo$bar", 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},
{"redirect param", "http://bar.foo.com/external-auth", "http://bar.foo.com/external-auth", "origUrl", "", "", "", "", false},
}
for _, test := range tests {
data[parser.GetAnnotationWithPrefix("auth-url")] = test.url
data[parser.GetAnnotationWithPrefix("auth-signin")] = test.signinURL
data[parser.GetAnnotationWithPrefix("auth-signin-redirect-param")] = test.signinURLRedirectParam
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
@ -122,6 +125,9 @@ func TestAnnotations(t *testing.T) {
if u.SigninURL != test.signinURL {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.signinURL, u.SigninURL)
}
if u.SigninURLRedirectParam != test.signinURLRedirectParam {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.signinURLRedirectParam, u.SigninURLRedirectParam)
}
if u.Method != test.method {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
}

View file

@ -704,7 +704,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, ""), "", "", "", []string{}, map[string]string{}}
defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", "", append(defResponseHeaders, ""), "", "", "", []string{}, map[string]string{}}
cfg := Configuration{
AllowBackendServerHeader: false,
@ -888,13 +888,14 @@ 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"`
AuthCacheKey string `json:"authCacheKey"`
AuthCacheDuration []string `json:"authCacheDuration"`
ProxySetHeaders map[string]string `json:"proxySetHeaders,omitempty"`
Host string `json:"host"`
SigninURL string `json:"signinUrl"`
SigninURLRedirectParam string `json:"signinUrlRedirectParam"`
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"`
ProxySetHeaders map[string]string `json:"proxySetHeaders,omitempty"`
}

View file

@ -37,31 +37,32 @@ import (
)
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"
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"
globalAuthCacheKey = "global-auth-cache-key"
globalAuthCacheDuration = "global-auth-cache-duration"
luaSharedDictsKey = "lua-shared-dicts"
plugins = "plugins"
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"
globalAuthSigninRedirectParam = "global-auth-signin-redirect-param"
globalAuthResponseHeaders = "global-auth-response-headers"
globalAuthRequestRedirect = "global-auth-request-redirect"
globalAuthSnippet = "global-auth-snippet"
globalAuthCacheKey = "global-auth-cache-key"
globalAuthCacheDuration = "global-auth-cache-duration"
luaSharedDictsKey = "lua-shared-dicts"
plugins = "plugins"
)
var (
@ -75,6 +76,7 @@ var (
"certificate_servers": 5,
"ocsp_response_cache": 5, // keep this same as certificate_servers
}
defaultGlobalAuthRedirectParam = "rd"
)
const (
@ -254,6 +256,19 @@ func ReadConfig(src map[string]string) config.Configuration {
}
}
// Verify that the configured global external authorization error page redirection URL parameter is set and valid. if not, set the default value
if val, ok := conf[globalAuthSigninRedirectParam]; ok {
delete(conf, globalAuthSigninRedirectParam)
redirectParam := strings.TrimSpace(val)
dummySigninURL, _ := parser.StringToURL(fmt.Sprintf("%s?%s=dummy", to.GlobalExternalAuth.SigninURL, redirectParam))
if dummySigninURL == nil {
klog.Warningf("Global auth redirect parameter denied - %v.", "global-auth-signin-redirect-param setting is invalid and will not be set")
} else {
to.GlobalExternalAuth.SigninURLRedirectParam = redirectParam
}
}
// 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)

View file

@ -229,6 +229,28 @@ func TestGlobalExternalAuthSigninParsing(t *testing.T) {
}
}
func TestGlobalExternalAuthSigninRedirectParamParsing(t *testing.T) {
testCases := map[string]struct {
param string
signin string
expect string
}{
"no param": {"", "http://bar.foo.com/auth-error-page", ""},
"valid param": {"orig", "http://bar.foo.com/auth-error-page", "orig"},
"no signin url": {"orig", "", ""},
}
for n, tc := range testCases {
cfg := ReadConfig(map[string]string{
"global-auth-signin": tc.signin,
"global-auth-signin-redirect-param": tc.param,
})
if cfg.GlobalExternalAuth.SigninURLRedirectParam != tc.expect {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", n, tc.expect, cfg.GlobalExternalAuth.SigninURLRedirectParam)
}
}
}
func TestGlobalExternalAuthResponseHeadersParsing(t *testing.T) {
testCases := map[string]struct {
headers string

View file

@ -50,9 +50,10 @@ import (
)
const (
slash = "/"
nonIdempotent = "non_idempotent"
defBufferSize = 65535
slash = "/"
nonIdempotent = "non_idempotent"
defBufferSize = 65535
defAuthSigninRedirectParam = "rd"
)
// TemplateWriter is the interface to render a template
@ -903,18 +904,21 @@ func buildForwardedFor(input interface{}) string {
return fmt.Sprintf("$http_%v", ffh)
}
func buildAuthSignURL(authSignURL string) string {
func buildAuthSignURL(authSignURL, authRedirectParam string) string {
u, _ := url.Parse(authSignURL)
q := u.Query()
if authRedirectParam == "" {
authRedirectParam = defaultGlobalAuthRedirectParam
}
if len(q) == 0 {
return fmt.Sprintf("%v?rd=$pass_access_scheme://$http_host$escaped_request_uri", authSignURL)
return fmt.Sprintf("%v?%v=$pass_access_scheme://$http_host$escaped_request_uri", authSignURL, authRedirectParam)
}
if q.Get("rd") != "" {
if q.Get(authRedirectParam) != "" {
return authSignURL
}
return fmt.Sprintf("%v&rd=$pass_access_scheme://$http_host$escaped_request_uri", authSignURL)
return fmt.Sprintf("%v&%v=$pass_access_scheme://$http_host$escaped_request_uri", authSignURL, authRedirectParam)
}
func buildAuthSignURLLocation(location, authSignURL string) string {

View file

@ -766,16 +766,19 @@ func TestFilterRateLimits(t *testing.T) {
func TestBuildAuthSignURL(t *testing.T) {
cases := map[string]struct {
Input, Output string
Input, RedirectParam, Output string
}{
"default url": {"http://google.com", "http://google.com?rd=$pass_access_scheme://$http_host$escaped_request_uri"},
"with random field": {"http://google.com?cat=0", "http://google.com?cat=0&rd=$pass_access_scheme://$http_host$escaped_request_uri"},
"with rd field": {"http://google.com?cat&rd=$request", "http://google.com?cat&rd=$request"},
"default url and redirect": {"http://google.com", "rd", "http://google.com?rd=$pass_access_scheme://$http_host$escaped_request_uri"},
"default url and custom redirect": {"http://google.com", "orig", "http://google.com?orig=$pass_access_scheme://$http_host$escaped_request_uri"},
"with random field": {"http://google.com?cat=0", "rd", "http://google.com?cat=0&rd=$pass_access_scheme://$http_host$escaped_request_uri"},
"with random field and custom redirect": {"http://google.com?cat=0", "orig", "http://google.com?cat=0&orig=$pass_access_scheme://$http_host$escaped_request_uri"},
"with rd field": {"http://google.com?cat&rd=$request", "rd", "http://google.com?cat&rd=$request"},
"with orig field": {"http://google.com?cat&orig=$request", "orig", "http://google.com?cat&orig=$request"},
}
for k, tc := range cases {
res := buildAuthSignURL(tc.Input)
res := buildAuthSignURL(tc.Input, tc.RedirectParam)
if res != tc.Output {
t.Errorf("%s: called buildAuthSignURL('%s'); expected '%v' but returned '%v'", k, tc.Input, tc.Output, res)
t.Errorf("%s: called buildAuthSignURL('%s','%s'); expected '%v' but returned '%v'", k, tc.Input, tc.RedirectParam, tc.Output, res)
}
}
}