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
9
docs/user-guide/nginx-configuration/annotations.md
Normal file → Executable file
9
docs/user-guide/nginx-configuration/annotations.md
Normal file → Executable file
|
@ -27,6 +27,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
|||
|[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-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|
|
||||
|[nginx.ingress.kubernetes.io/canary](#canary)|"true" or "false"|
|
||||
|[nginx.ingress.kubernetes.io/canary-by-header](#canary)|string|
|
||||
|
@ -389,6 +390,14 @@ nginx.ingress.kubernetes.io/auth-snippet: |
|
|||
!!! example
|
||||
Please check the [external-auth](../../examples/auth/external-auth/README.md) example.
|
||||
|
||||
#### Global External Authentication
|
||||
|
||||
By default the controller redirects all requests to an existing service that provides authentication if `global-auth-url` is set in the NGINX ConfigMap. If you want to disable this behavior for that ingress, you can use ssl-redirect: "false" in the NGINX ConfigMap.
|
||||
`nginx.ingress.kubernetes.io/enable-global-auth`:
|
||||
indicates if GlobalExternalAuth configuration should be applied or not to this Ingress rule. Default values is set to `"true"`.
|
||||
|
||||
!!! note For more information please see [global-auth-url](./configmap.md#global-auth-url).
|
||||
|
||||
### Rate limiting
|
||||
|
||||
These annotations define a limit on the connections that can be opened by a single client IP address.
|
||||
|
|
45
docs/user-guide/nginx-configuration/configmap.md
Normal file → Executable file
45
docs/user-guide/nginx-configuration/configmap.md
Normal file → Executable file
|
@ -152,6 +152,12 @@ The following table shows a configuration option's name, type, and the default v
|
|||
|[limit-req-status-code](#limit-req-status-code)|int|503|
|
||||
|[limit-conn-status-code](#limit-conn-status-code)|int|503|
|
||||
|[no-tls-redirect-locations](#no-tls-redirect-locations)|string|"/.well-known/acme-challenge"|
|
||||
|[global-auth-url](#global-auth-url)|string|""|
|
||||
|[global-auth-method](#global-auth-method)|string|""|
|
||||
|[global-auth-signin](#global-auth-signin)|string|""|
|
||||
|[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|""|
|
||||
|[no-auth-locations](#no-auth-locations)|string|"/.well-known/acme-challenge"|
|
||||
|[block-cidrs](#block-cidrs)|[]string|""|
|
||||
|[block-user-agents](#block-user-agents)|[]string|""|
|
||||
|
@ -864,6 +870,45 @@ Sets the [status code to return in response to rejected connections](http://ngin
|
|||
A comma-separated list of locations on which http requests will never get redirected to their https counterpart.
|
||||
_**default:**_ "/.well-known/acme-challenge"
|
||||
|
||||
## global-auth-url
|
||||
|
||||
A url to an existing service that provides authentication for all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-url`.
|
||||
Locations that should not get authenticated can be listed using `no-auth-locations` See [no-auth-locations](#no-auth-locations). In addition, each service can be excluded from authentication via annotation `enable-global-auth` set to "false".
|
||||
_**default:**_ ""
|
||||
|
||||
_References:_ [https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md#external-authentication](https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md#external-authentication)
|
||||
|
||||
## global-auth-method
|
||||
|
||||
A HTTP method to use for an existing service that provides authentication for all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-method`.
|
||||
_**default:**_ ""
|
||||
|
||||
## global-auth-signin
|
||||
|
||||
Sets the location of the error page for an existing service that provides authentication for all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-signin`.
|
||||
_**default:**_ ""
|
||||
|
||||
## global-auth-response-headers
|
||||
|
||||
Sets the headers to pass to backend once authentication request completes. Applied to all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-response-headers`.
|
||||
_**default:**_ ""
|
||||
|
||||
## global-auth-request-redirect
|
||||
|
||||
Sets the X-Auth-Request-Redirect header value. Applied to all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-request-redirect`.
|
||||
_**default:**_ ""
|
||||
|
||||
## global-auth-snippet
|
||||
|
||||
Sets a custom snippet to use with external authentication. Applied to all the locations.
|
||||
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/auth-request-redirect`.
|
||||
_**default:**_ ""
|
||||
|
||||
## no-auth-locations
|
||||
|
||||
A comma-separated list of locations that should not get authenticated.
|
||||
|
|
3
internal/ingress/annotations/annotations.go
Normal file → Executable file
3
internal/ingress/annotations/annotations.go
Normal file → Executable file
|
@ -30,6 +30,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/alias"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/auth"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/authreqglobal"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/authtls"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/backendprotocol"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/clientbodybuffersize"
|
||||
|
@ -83,6 +84,7 @@ type Ingress struct {
|
|||
//TODO: Change this back into an error when https://github.com/imdario/mergo/issues/100 is resolved
|
||||
Denied *string
|
||||
ExternalAuth authreq.Config
|
||||
EnableGlobalAuth bool
|
||||
HTTP2PushPreload bool
|
||||
Proxy proxy.Config
|
||||
RateLimit ratelimit.Config
|
||||
|
@ -127,6 +129,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"CustomHTTPErrors": customhttperrors.NewParser(cfg),
|
||||
"DefaultBackend": defaultbackend.NewParser(cfg),
|
||||
"ExternalAuth": authreq.NewParser(cfg),
|
||||
"EnableGlobalAuth": authreqglobal.NewParser(cfg),
|
||||
"HTTP2PushPreload": http2pushpreload.NewParser(cfg),
|
||||
"Proxy": proxy.NewParser(cfg),
|
||||
"RateLimit": ratelimit.NewParser(cfg),
|
||||
|
|
45
internal/ingress/annotations/authreq/main.go
Normal file → Executable file
45
internal/ingress/annotations/authreq/main.go
Normal file → Executable file
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package authreq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -84,7 +85,8 @@ var (
|
|||
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
|
||||
)
|
||||
|
||||
func validMethod(method string) bool {
|
||||
// ValidMethod checks is the provided string a valid HTTP method
|
||||
func ValidMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
@ -97,7 +99,8 @@ func validMethod(method string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func validHeader(header string) bool {
|
||||
// ValidHeader checks is the provided string satisfies the header's name regex
|
||||
func ValidHeader(header string) bool {
|
||||
return headerRegexp.Match([]byte(header))
|
||||
}
|
||||
|
||||
|
@ -119,22 +122,13 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
authURL, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authURL.Scheme == "" {
|
||||
return nil, ing_errors.NewLocationDenied("url scheme is empty")
|
||||
}
|
||||
if authURL.Host == "" {
|
||||
return nil, ing_errors.NewLocationDenied("url host is empty")
|
||||
}
|
||||
if strings.Contains(authURL.Host, "..") {
|
||||
return nil, ing_errors.NewLocationDenied("invalid url host")
|
||||
authURL, message := ParseStringToURL(urlString)
|
||||
if authURL == nil {
|
||||
return nil, ing_errors.NewLocationDenied(message)
|
||||
}
|
||||
|
||||
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
|
||||
if len(authMethod) != 0 && !validMethod(authMethod) {
|
||||
if len(authMethod) != 0 && !ValidMethod(authMethod) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
}
|
||||
|
||||
|
@ -156,7 +150,7 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
|
|||
for _, header := range harr {
|
||||
header = strings.TrimSpace(header)
|
||||
if len(header) > 0 {
|
||||
if !validHeader(header) {
|
||||
if !ValidHeader(header) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid headers list")
|
||||
}
|
||||
responseHeaders = append(responseHeaders, header)
|
||||
|
@ -176,3 +170,22 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
|
|||
AuthSnippet: authSnippet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseStringToURL parses the provided string into URL and returns error
|
||||
// message in case of failure
|
||||
func ParseStringToURL(input string) (*url.URL, string) {
|
||||
|
||||
parsedURL, err := url.Parse(input)
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("%v is not a valid URL: %v", input, err)
|
||||
}
|
||||
if parsedURL.Scheme == "" {
|
||||
return nil, "url scheme is empty."
|
||||
} else if parsedURL.Host == "" {
|
||||
return nil, "url host is empty."
|
||||
} else if strings.Contains(parsedURL.Host, "..") {
|
||||
return nil, "invalid url host."
|
||||
}
|
||||
return parsedURL, ""
|
||||
|
||||
}
|
||||
|
|
36
internal/ingress/annotations/authreq/main_test.go
Normal file → Executable file
36
internal/ingress/annotations/authreq/main_test.go
Normal file → Executable file
|
@ -18,6 +18,7 @@ package authreq
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -178,3 +179,38 @@ func TestHeaderAnnotations(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringToURL(t *testing.T) {
|
||||
validURL := "http://bar.foo.com/external-auth"
|
||||
validParsedURL, _ := url.Parse(validURL)
|
||||
|
||||
tests := []struct {
|
||||
title string
|
||||
url string
|
||||
message string
|
||||
parsed *url.URL
|
||||
expErr bool
|
||||
}{
|
||||
{"empty", "", "url scheme is empty.", nil, true},
|
||||
{"no scheme", "bar", "url scheme is empty.", nil, true},
|
||||
{"invalid host", "http://", "url host is empty.", nil, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host.", nil, true},
|
||||
{"valid URL", validURL, "", validParsedURL, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
i, err := ParseStringToURL(test.url)
|
||||
if test.expErr {
|
||||
if err != test.message {
|
||||
t.Errorf("%v: expected error \"%v\" but \"%v\" was returned", test.title, test.message, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i.String() != test.parsed.String() {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.parsed, i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
45
internal/ingress/annotations/authreqglobal/main.go
Executable file
45
internal/ingress/annotations/authreqglobal/main.go
Executable file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authreqglobal
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
type authReqGlobal struct {
|
||||
r resolver.Resolver
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReqGlobal{r}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to enable or disable global external authentication
|
||||
func (a authReqGlobal) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
|
||||
enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing)
|
||||
if err != nil {
|
||||
enableGlobalAuth = true
|
||||
}
|
||||
|
||||
return enableGlobalAuth, nil
|
||||
}
|
82
internal/ingress/annotations/authreqglobal/main_test.go
Executable file
82
internal/ingress/annotations/authreqglobal/main_test.go
Executable file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authreqglobal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Spec: extensions.IngressSpec{
|
||||
Backend: &extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Rules: []extensions.IngressRule{
|
||||
{
|
||||
Host: "foo.bar.com",
|
||||
IngressRuleValue: extensions.IngressRuleValue{
|
||||
HTTP: &extensions.HTTPIngressRuleValue{
|
||||
Paths: []extensions.HTTPIngressPath{
|
||||
{
|
||||
Path: "/foo",
|
||||
Backend: defaultBackend,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-url")] = "http://foo.com/external-auth"
|
||||
data[parser.GetAnnotationWithPrefix("enable-global-auth")] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
u, ok := i.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a Config type")
|
||||
}
|
||||
if u {
|
||||
t.Errorf("Expected false but returned true")
|
||||
}
|
||||
}
|
21
internal/ingress/controller/config/config.go
Normal file → Executable file
21
internal/ingress/controller/config/config.go
Normal file → Executable file
|
@ -563,6 +563,11 @@ type Configuration struct {
|
|||
// should not get authenticated
|
||||
NoAuthLocations string `json:"no-auth-locations"`
|
||||
|
||||
// GlobalExternalAuth indicates the access to all locations requires
|
||||
// authentication using an external provider
|
||||
// +optional
|
||||
GlobalExternalAuth GlobalExternalAuth `json:"global-external-auth"`
|
||||
|
||||
// DisableLuaRestyWAF disables lua-resty-waf globally regardless
|
||||
// of whether there's an ingress that has enabled the WAF using annotation
|
||||
DisableLuaRestyWAF bool `json:"disable-lua-resty-waf"`
|
||||
|
@ -592,11 +597,13 @@ func NewDefault() Configuration {
|
|||
defBlockEntity := make([]string, 0)
|
||||
defNginxStatusIpv4Whitelist := make([]string, 0)
|
||||
defNginxStatusIpv6Whitelist := make([]string, 0)
|
||||
defResponseHeaders := make([]string, 0)
|
||||
|
||||
defIPCIDR = append(defIPCIDR, "0.0.0.0/0")
|
||||
defNginxStatusIpv4Whitelist = append(defNginxStatusIpv4Whitelist, "127.0.0.1")
|
||||
defNginxStatusIpv6Whitelist = append(defNginxStatusIpv6Whitelist, "::1")
|
||||
defProxyDeadlineDuration := time.Duration(5) * time.Second
|
||||
degGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", append(defResponseHeaders, ""), "", ""}
|
||||
|
||||
cfg := Configuration{
|
||||
AllowBackendServerHeader: false,
|
||||
|
@ -715,6 +722,7 @@ func NewDefault() Configuration {
|
|||
SyslogPort: 514,
|
||||
NoTLSRedirectLocations: "/.well-known/acme-challenge",
|
||||
NoAuthLocations: "/.well-known/acme-challenge",
|
||||
GlobalExternalAuth: degGlobalExternalAuth,
|
||||
}
|
||||
|
||||
if klog.V(5) {
|
||||
|
@ -772,3 +780,16 @@ type ListenPorts struct {
|
|||
Default int
|
||||
SSLProxy int
|
||||
}
|
||||
|
||||
// GlobalExternalAuth describe external authentication configuration for the
|
||||
// NGINX Ingress controller
|
||||
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"`
|
||||
}
|
||||
|
|
3
internal/ingress/controller/controller.go
Normal file → Executable file
3
internal/ingress/controller/controller.go
Normal file → Executable file
|
@ -98,6 +98,8 @@ type Configuration struct {
|
|||
ValidationWebhook string
|
||||
ValidationWebhookCertPath string
|
||||
ValidationWebhookKeyPath string
|
||||
|
||||
GlobalExternalAuth *ngx_config.GlobalExternalAuth
|
||||
}
|
||||
|
||||
// GetPublishService returns the Service used to set the load-balancer status of Ingresses.
|
||||
|
@ -1151,6 +1153,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
|
|||
loc.ConfigurationSnippet = anns.ConfigurationSnippet
|
||||
loc.CorsConfig = anns.CorsConfig
|
||||
loc.ExternalAuth = anns.ExternalAuth
|
||||
loc.EnableGlobalAuth = anns.EnableGlobalAuth
|
||||
loc.HTTP2PushPreload = anns.HTTP2PushPreload
|
||||
loc.Proxy = anns.Proxy
|
||||
loc.RateLimit = anns.RateLimit
|
||||
|
|
0
internal/ingress/controller/nginx.go
Normal file → Executable file
0
internal/ingress/controller/nginx.go
Normal file → Executable file
0
internal/ingress/controller/store/store.go
Normal file → Executable file
0
internal/ingress/controller/store/store.go
Normal file → Executable file
0
internal/ingress/controller/store/store_test.go
Normal file → Executable file
0
internal/ingress/controller/store/store_test.go
Normal file → Executable file
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)
|
||||
}
|
||||
|
|
3
internal/ingress/types.go
Normal file → Executable file
3
internal/ingress/types.go
Normal file → Executable file
|
@ -246,6 +246,9 @@ type Location struct {
|
|||
// authentication using an external provider
|
||||
// +optional
|
||||
ExternalAuth authreq.Config `json:"externalAuth,omitempty"`
|
||||
// EnableGlobalAuth indicates if the access to this location requires
|
||||
// authentication using an external provider defined in controller's config
|
||||
EnableGlobalAuth bool `json:"enableGlobalAuth"`
|
||||
// HTTP2PushPreload allows to configure the HTTP2 Push Preload from backend
|
||||
// original location.
|
||||
// +optional
|
||||
|
|
3
internal/ingress/types_equals.go
Normal file → Executable file
3
internal/ingress/types_equals.go
Normal file → Executable file
|
@ -352,6 +352,9 @@ func (l1 *Location) Equal(l2 *Location) bool {
|
|||
if !(&l1.ExternalAuth).Equal(&l2.ExternalAuth) {
|
||||
return false
|
||||
}
|
||||
if l1.EnableGlobalAuth != l2.EnableGlobalAuth {
|
||||
return false
|
||||
}
|
||||
if l1.HTTP2PushPreload != l2.HTTP2PushPreload {
|
||||
return false
|
||||
}
|
||||
|
|
31
rootfs/etc/nginx/template/nginx.tmpl
Normal file → Executable file
31
rootfs/etc/nginx/template/nginx.tmpl
Normal file → Executable file
|
@ -894,7 +894,13 @@ stream {
|
|||
{{ range $location := $server.Locations }}
|
||||
{{ $path := buildLocation $location $enforceRegex }}
|
||||
{{ $proxySetHeader := proxySetHeader $location }}
|
||||
{{ $authPath := buildAuthLocation $location }}
|
||||
{{ $authPath := buildAuthLocation $location $all.Cfg.GlobalExternalAuth.URL }}
|
||||
{{ $applyGlobalAuth := shouldApplyGlobalAuth $location $all.Cfg.GlobalExternalAuth.URL }}
|
||||
|
||||
{{ $externalAuth := $location.ExternalAuth }}
|
||||
{{ if eq $applyGlobalAuth true }}
|
||||
{{ $externalAuth = $all.Cfg.GlobalExternalAuth }}
|
||||
{{ end }}
|
||||
|
||||
{{ if not (empty $location.Rewrite.AppRoot)}}
|
||||
if ($uri = /) {
|
||||
|
@ -915,13 +921,13 @@ stream {
|
|||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Forwarded-Proto "";
|
||||
|
||||
{{ if $location.ExternalAuth.Method }}
|
||||
proxy_method {{ $location.ExternalAuth.Method }};
|
||||
{{ if $externalAuth.Method }}
|
||||
proxy_method {{ $externalAuth.Method }};
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
proxy_set_header X-Scheme $pass_access_scheme;
|
||||
{{ end }}
|
||||
|
||||
proxy_set_header Host {{ $location.ExternalAuth.Host }};
|
||||
proxy_set_header Host {{ $externalAuth.Host }};
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
proxy_set_header X-Sent-From "nginx-ingress-controller";
|
||||
|
@ -932,8 +938,8 @@ stream {
|
|||
proxy_set_header X-Forwarded-For $the_real_ip;
|
||||
{{ end }}
|
||||
|
||||
{{ if $location.ExternalAuth.RequestRedirect }}
|
||||
proxy_set_header X-Auth-Request-Redirect {{ $location.ExternalAuth.RequestRedirect }};
|
||||
{{ if $externalAuth.RequestRedirect }}
|
||||
proxy_set_header X-Auth-Request-Redirect {{ $externalAuth.RequestRedirect }};
|
||||
{{ else }}
|
||||
proxy_set_header X-Auth-Request-Redirect $request_uri;
|
||||
{{ end }}
|
||||
|
@ -963,15 +969,16 @@ stream {
|
|||
proxy_set_header ssl-client-issuer-dn $ssl_client_i_dn;
|
||||
{{ end }}
|
||||
|
||||
{{ if not (empty $location.ExternalAuth.AuthSnippet) }}
|
||||
{{ $location.ExternalAuth.AuthSnippet }}
|
||||
{{ if not (empty $externalAuth.AuthSnippet) }}
|
||||
{{ $externalAuth.AuthSnippet }}
|
||||
{{ end }}
|
||||
|
||||
set $target {{ $location.ExternalAuth.URL }};
|
||||
set $target {{ $externalAuth.URL }};
|
||||
proxy_pass $target;
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
location {{ $path }} {
|
||||
{{ $ing := (getIngressInformation $location.Ingress $server.Hostname $location.Path) }}
|
||||
set $namespace "{{ $ing.Namespace }}";
|
||||
|
@ -1125,14 +1132,14 @@ stream {
|
|||
auth_request {{ $authPath }};
|
||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||
add_header Set-Cookie $auth_cookie;
|
||||
{{- range $line := buildAuthResponseHeaders $location }}
|
||||
{{- range $line := buildAuthResponseHeaders $externalAuth.ResponseHeaders }}
|
||||
{{ $line }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $location.ExternalAuth.SigninURL }}
|
||||
{{ if $externalAuth.SigninURL }}
|
||||
set_escape_uri $escaped_request_uri $request_uri;
|
||||
error_page 401 = {{ buildAuthSignURL $location.ExternalAuth.SigninURL }};
|
||||
error_page 401 = {{ buildAuthSignURL $externalAuth.SigninURL }};
|
||||
{{ end }}
|
||||
|
||||
{{ if $location.BasicDigestAuth.Secured }}
|
||||
|
|
217
test/e2e/settings/global_external_auth.go
Executable file
217
test/e2e/settings/global_external_auth.go
Executable file
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/parnurzeal/gorequest"
|
||||
|
||||
"k8s.io/ingress-nginx/test/e2e/framework"
|
||||
)
|
||||
|
||||
var _ = framework.IngressNginxDescribe("Global External Auth", func() {
|
||||
f := framework.NewDefaultFramework("global-external-auth")
|
||||
|
||||
host := "global-external-auth"
|
||||
|
||||
echoServiceName := "http-svc"
|
||||
|
||||
globalExternalAuthURLSetting := "global-auth-url"
|
||||
|
||||
fooPath := "/foo"
|
||||
barPath := "/bar"
|
||||
|
||||
noAuthSetting := "no-auth-locations"
|
||||
noAuthLocations := barPath
|
||||
|
||||
enableGlobalExternalAuthAnnotation := "nginx.ingress.kubernetes.io/enable-global-auth"
|
||||
|
||||
BeforeEach(func() {
|
||||
f.NewEchoDeployment()
|
||||
f.NewHttpbinDeployment()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
})
|
||||
|
||||
Context("when global external authentication is configured", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
globalExternalAuthURL := fmt.Sprintf("http://httpbin.%s.svc.cluster.local:80/status/401", f.Namespace)
|
||||
|
||||
By("Adding an ingress rule for /foo")
|
||||
fooIng := framework.NewSingleIngress("foo-ingress", fooPath, host, f.Namespace, echoServiceName, 80, nil)
|
||||
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("bar-ingress", barPath, host, f.Namespace, echoServiceName, 80, nil)
|
||||
f.EnsureIngress(barIng)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring("location /bar"))
|
||||
})
|
||||
|
||||
By("Adding a global-auth-url to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthURLSetting, globalExternalAuthURL)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring(globalExternalAuthURL))
|
||||
})
|
||||
})
|
||||
|
||||
It("should return status code 401 when request any protected service", func() {
|
||||
|
||||
By("Sending a request to protected service /foo")
|
||||
fooResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+fooPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized))
|
||||
|
||||
By("Sending a request to protected service /bar")
|
||||
barResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+barPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(barResp.StatusCode).Should(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
|
||||
It("should return status code 200 when request whitelisted (via no-auth-locations) service and 401 when request protected service", func() {
|
||||
|
||||
By("Adding a no-auth-locations for /bar to configMap")
|
||||
f.UpdateNginxConfigMapData(noAuthSetting, noAuthLocations)
|
||||
|
||||
By("Sending a request to protected service /foo")
|
||||
fooResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+fooPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized))
|
||||
|
||||
By("Sending a request to whitelisted service /bar")
|
||||
barResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+barPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(barResp.StatusCode).Should(Equal(http.StatusOK))
|
||||
})
|
||||
|
||||
It("should return status code 200 when request whitelisted (via ingress annotation) service and 401 when request protected service", func() {
|
||||
|
||||
By("Adding an ingress rule for /bar with annotation enable-global-auth = false")
|
||||
annotations := map[string]string{
|
||||
enableGlobalExternalAuthAnnotation: "false",
|
||||
}
|
||||
barIng := framework.NewSingleIngress("bar-ingress", barPath, host, f.Namespace, echoServiceName, 80, &annotations)
|
||||
f.EnsureIngress(barIng)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring("location /bar"))
|
||||
})
|
||||
|
||||
By("Sending a request to protected service /foo")
|
||||
fooResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+fooPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(fooResp.StatusCode).Should(Equal(http.StatusUnauthorized))
|
||||
|
||||
By("Sending a request to whitelisted service /bar")
|
||||
barResp, _, _ := gorequest.New().
|
||||
Get(f.GetURL(framework.HTTP)+barPath).
|
||||
Set("Host", host).
|
||||
End()
|
||||
Expect(barResp.StatusCode).Should(Equal(http.StatusOK))
|
||||
})
|
||||
|
||||
It(`should proxy_method method when global-auth-method is configured`, func() {
|
||||
|
||||
globalExternalAuthMethodSetting := "global-auth-method"
|
||||
globalExternalAuthMethod := "GET"
|
||||
|
||||
By("Adding a global-auth-method to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthMethodSetting, globalExternalAuthMethod)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring("proxy_method"))
|
||||
})
|
||||
})
|
||||
|
||||
It(`should add custom error page when global-auth-signin url is configured`, func() {
|
||||
|
||||
globalExternalAuthSigninSetting := "global-auth-signin"
|
||||
globalExternalAuthSignin := "http://foo.com/global-error-page"
|
||||
|
||||
By("Adding a global-auth-signin to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthSigninSetting, globalExternalAuthSignin)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring("error_page 401 = "))
|
||||
})
|
||||
})
|
||||
|
||||
It(`should add auth headers when global-auth-response-headers is configured`, func() {
|
||||
|
||||
globalExternalAuthResponseHeadersSetting := "global-auth-response-headers"
|
||||
globalExternalAuthResponseHeaders := "Foo, Bar"
|
||||
|
||||
By("Adding a global-auth-response-headers to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthResponseHeadersSetting, globalExternalAuthResponseHeaders)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring("auth_request_set $authHeader0 $upstream_http_foo;")) &&
|
||||
Expect(server).Should(ContainSubstring("auth_request_set $authHeader1 $upstream_http_bar;"))
|
||||
})
|
||||
})
|
||||
|
||||
It(`should set request-redirect when global-auth-request-redirect is configured`, func() {
|
||||
|
||||
globalExternalAuthRequestRedirectSetting := "global-auth-request-redirect"
|
||||
globalExternalAuthRequestRedirect := "Foo-Redirect"
|
||||
|
||||
By("Adding a global-auth-request-redirect to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthRequestRedirectSetting, globalExternalAuthRequestRedirect)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring(globalExternalAuthRequestRedirect))
|
||||
})
|
||||
})
|
||||
|
||||
It(`should set snippet when global external auth is configured`, func() {
|
||||
|
||||
globalExternalAuthSnippetSetting := "global-auth-snippet"
|
||||
globalExternalAuthSnippet := "proxy_set_header My-Custom-Header 42;"
|
||||
|
||||
By("Adding a global-auth-snippet to configMap")
|
||||
f.UpdateNginxConfigMapData(globalExternalAuthSnippetSetting, globalExternalAuthSnippet)
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return Expect(server).Should(ContainSubstring(globalExternalAuthSnippet))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in a new issue