Added Global External Authentication settings to configmap parameters incl. addons

This commit is contained in:
okryvoshapka-connyun 2018-11-27 17:12:17 +01:00
parent b4f2880ee6
commit 8cc9afe8ee
20 changed files with 819 additions and 72 deletions

106
internal/ingress/controller/template/configmap.go Normal file → Executable file
View file

@ -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)

118
internal/ingress/controller/template/configmap_test.go Normal file → Executable file
View file

@ -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)
}
}
}

29
internal/ingress/controller/template/template.go Normal file → Executable file
View file

@ -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)
}
if (location.ExternalAuth.URL == "") && (globalExternalAuthURL != "") && (location.EnableGlobalAuth) {
return true
}
return false
}
func buildAuthResponseHeaders(headers []string) []string {
res := []string{}
if len(headers) == 0 {
return res
}
if len(location.ExternalAuth.ResponseHeaders) == 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))

95
internal/ingress/controller/template/template_test.go Normal file → Executable file
View file

@ -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, ""},
}
for _, testCase := range testCases {
loc.ExternalAuth.URL = testCase.authURL
loc.EnableGlobalAuth = testCase.enableglobalExternalAuth
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{
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)
}
}
}
func TestBuildAuthResponseHeaders(t *testing.T) {
invalidType := &ingress.Ingress{}
expected := []string{}
actual := buildAuthResponseHeaders(invalidType)
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Expected '%v' but returned '%v'", expected, actual)
}
loc := &ingress.Location{
ExternalAuth: authreq.Config{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}},
}
headers := buildAuthResponseHeaders(loc)
expected = []string{
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)
}