Added Global External Authentication settings to configmap parameters incl. addons
This commit is contained in:
parent
b4f2880ee6
commit
8cc9afe8ee
20 changed files with 819 additions and 72 deletions
106
internal/ingress/controller/template/configmap.go
Normal file → Executable file
106
internal/ingress/controller/template/configmap.go
Normal file → Executable 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
118
internal/ingress/controller/template/configmap_test.go
Normal file → Executable 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
29
internal/ingress/controller/template/template.go
Normal file → Executable 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
95
internal/ingress/controller/template/template_test.go
Normal file → Executable 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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue