From af5754bcab4ec4e1e4ce7c0947cd0f32274b1cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Michaud?= Date: Wed, 6 May 2020 03:50:37 +0200 Subject: [PATCH] [MISC] Add coverage for Remote-User and Remote-Groups (#982) * Fix dev workflow. * Fix dev workflow. * Cover Remote-User and Remote-Groups using Traefik. * Cover Remote-User and Remote-Groups using HAProxy. * Fix redirection after unauthorized response when using HAProxy. Co-authored-by: Amir Zarrinkafsh --- internal/server/public_html.gen.go | 5 +- .../authelia/docker-compose.backend.dev.yml | 1 + .../suites/example/compose/haproxy/Dockerfile | 1 - .../example/compose/haproxy/auth-request.lua | 103 ++++++++++++++++++ .../compose/haproxy/docker-compose.yml | 1 + .../example/compose/haproxy/haproxy.cfg | 13 ++- .../compose/httpbin/docker-compose.yml | 17 +++ .../example/compose/nginx/portal/nginx.conf | 4 +- .../suites/scenario_custom_headers_test.go | 11 +- internal/suites/suite_haproxy.go | 1 + internal/suites/suite_haproxy_test.go | 4 + internal/suites/suite_traefik.go | 1 + internal/suites/suite_traefik2.go | 1 + internal/suites/suite_traefik2_test.go | 4 + internal/suites/suite_traefik_test.go | 4 + 15 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 internal/suites/example/compose/haproxy/auth-request.lua diff --git a/internal/server/public_html.gen.go b/internal/server/public_html.gen.go index d3c354df5..509991096 100644 --- a/internal/server/public_html.gen.go +++ b/internal/server/public_html.gen.go @@ -2,5 +2,6 @@ package server import "aletheia.icu/broccoli/fs" -// Mock the embedded filesystem for unit tests. -var br = fs.New(false, []byte("")) +// Mock the embedded filesystem for unit tests. The bundle is built from an empty file and +// allows to run the dev workflow without failure. +var br = fs.New(false, []byte("\x1b~\x00\x80\x8d\x94n\xc2|\x84J\xf7\xbfn\xfd\xf7w;.\x8d m\xb2&\xd1Z\xec\xb2\x05\xb9\xc00\x8a\xf7(\x80^78\t(\f\f\xc3p\xc2\xc1\x06[a\xa2\xb3\xa4P\xe5\xa14\xfb\x19\xb2cp\xf6\x90-Z\xb2\x11\xe0l\xa1\x80\\\x95Vh\t\xc5\x06\x16\xfa\x8c\xc0\"!\xa5\xcf\xf7$\x9a\xb2\a`\xc6\x18\xc8~\xce8\r\x16Z\x9d\xc3\xe3\xff\x00")) diff --git a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml index 02a473f5b..a9781dd74 100644 --- a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml +++ b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml @@ -16,6 +16,7 @@ services: labels: # Traefik 1.x - 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api' + - 'traefik.protocol=https' # Traefik 2.x - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/api`)' - 'traefik.http.routers.authelia_backend.entrypoints=https' diff --git a/internal/suites/example/compose/haproxy/Dockerfile b/internal/suites/example/compose/haproxy/Dockerfile index b5f52ebd0..a85535a19 100644 --- a/internal/suites/example/compose/haproxy/Dockerfile +++ b/internal/suites/example/compose/haproxy/Dockerfile @@ -5,6 +5,5 @@ apk add --no-cache \ curl \ lua5.3-socket \ openssl && \ -curl -Lfs -o /usr/local/etc/haproxy/auth-request.lua "https://raw.githubusercontent.com/TimWolla/haproxy-auth-request/master/auth-request.lua" && \ openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=AU/ST=Victoria/L=Melbourne/O=Authelia/CN=*.example.com" -keyout haproxy.key -out haproxy.crt && \ cat haproxy.key haproxy.crt > /usr/local/etc/haproxy/haproxy.pem \ No newline at end of file diff --git a/internal/suites/example/compose/haproxy/auth-request.lua b/internal/suites/example/compose/haproxy/auth-request.lua new file mode 100644 index 000000000..debb81958 --- /dev/null +++ b/internal/suites/example/compose/haproxy/auth-request.lua @@ -0,0 +1,103 @@ +-- The MIT License (MIT) +-- +-- Copyright (c) 2018 Tim Düsterhus +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +local http = require("socket.http") + +core.register_action("auth-request", { "http-req" }, function(txn, be, path) + txn:set_var("txn.auth_response_successful", false) + + -- Check whether the given backend exists. + if core.backends[be] == nil then + txn:Alert("Unknown auth-request backend '" .. be .. "'") + txn:set_var("txn.auth_response_code", 500) + return + end + + -- Check whether the given backend has servers that + -- are not `DOWN`. + local addr = nil + for name, server in pairs(core.backends[be].servers) do + local status = server:get_stats()['status'] + if status == "no check" or status:find("UP") == 1 then + addr = server:get_addr() + break + end + end + if addr == nil then + txn:Warning("No servers available for auth-request backend: '" .. be .. "'") + txn:set_var("txn.auth_response_code", 500) + return + end + + -- Transform table of request headers from haproxy's to + -- socket.http's format. + local headers = {} + for header, values in pairs(txn.http:req_get_headers()) do + if header ~= 'content-length' then + for i, v in pairs(values) do + if headers[header] == nil then + headers[header] = v + else + headers[header] = headers[header] .. ", " .. v + end + end + end + end + + -- Make request to backend. + local b, c, h = http.request { + url = "http://" .. addr .. path, + headers = headers, + create = core.tcp, + -- Disable redirects, because DNS does not work here. + redirect = false, + -- We do not check body, so HEAD + method = "HEAD", + } + + -- Check whether we received a valid HTTP response. + if b == nil then + txn:Warning("Failure in auth-request backend '" .. be .. "': " .. c) + txn:set_var("txn.auth_response_code", 500) + return + end + + txn:set_var("txn.auth_response_code", c) + + -- 2xx: Allow request. + if 200 <= c and c < 300 then + if h["remote-user"] then + txn:set_var("txn.auth_user", h["remote-user"]) + end + if h["remote-groups"] then + txn:set_var("txn.auth_groups", h["remote-groups"]) + end + txn:set_var("txn.auth_response_successful", true) + -- Don't allow other codes. + -- Codes with Location: Passthrough location at redirect. + elseif c == 301 or c == 302 or c == 303 or c == 307 or c == 308 then + txn:set_var("txn.auth_response_location", h["location"]) + -- 401 / 403: Do nothing, everything else: log. + elseif c ~= 401 and c ~= 403 then + txn:Warning("Invalid status code in auth-request backend '" .. be .. "': " .. c) + end +end, 2) \ No newline at end of file diff --git a/internal/suites/example/compose/haproxy/docker-compose.yml b/internal/suites/example/compose/haproxy/docker-compose.yml index 90ad45b5a..452bd0949 100644 --- a/internal/suites/example/compose/haproxy/docker-compose.yml +++ b/internal/suites/example/compose/haproxy/docker-compose.yml @@ -4,6 +4,7 @@ services: build: ./example/compose/haproxy/ volumes: - ./example/compose/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + - ./example/compose/haproxy/auth-request.lua:/usr/local/etc/haproxy/auth-request.lua networks: authelianet: # Set the IP to be able to query on port 8080 diff --git a/internal/suites/example/compose/haproxy/haproxy.cfg b/internal/suites/example/compose/haproxy/haproxy.cfg index b5b1ea9b2..ef18ac111 100644 --- a/internal/suites/example/compose/haproxy/haproxy.cfg +++ b/internal/suites/example/compose/haproxy/haproxy.cfg @@ -22,6 +22,7 @@ frontend fe_http acl host-authelia-portal hdr(host) -i login.example.com:8080 acl api-path path_beg -i /api acl protected-frontends hdr(host) -m reg -i ^(admin|home|public|secure|singlefactor)\.example\.com + acl is_headers path -i -m end /headers http-request set-var(req.scheme) str(https) if { ssl_fc } http-request set-var(req.scheme) str(http) if !{ ssl_fc } @@ -37,9 +38,11 @@ frontend fe_http # does not know how to handle it (see https://github.com/TimWolla/haproxy-auth-request/issues/12). http-request lua.auth-request be_auth_request /api/verify if protected-frontends + http-request redirect location https://login.example.com:8080 if protected-frontends !{ var(txn.auth_response_successful) -m bool } + use_backend be_authelia if host-authelia-portal api-path use_backend fe_authelia if host-authelia-portal !api-path - use_backend be_authelia if protected-frontends !{ var(txn.auth_response_successful) -m bool } + use_backend be_httpbin if protected-frontends is_headers use_backend be_protected if protected-frontends use_backend be_mail if { hdr(host) -i mail.example.com:8080 } @@ -63,3 +66,11 @@ backend be_mail backend be_protected server nginx-backend nginx-backend:80 + +backend be_httpbin + acl remote_user_exist var(txn.auth_user) -m found + acl remote_groups_exist var(txn.auth_groups) -m found + + http-request set-header Remote-User %[var(txn.auth_user)] if remote_user_exist + http-request set-header Remote-Groups %[var(txn.auth_groups)] if remote_groups_exist + server httpbin-backend httpbin:8000 \ No newline at end of file diff --git a/internal/suites/example/compose/httpbin/docker-compose.yml b/internal/suites/example/compose/httpbin/docker-compose.yml index 48b3e34b4..1f64ca3b7 100644 --- a/internal/suites/example/compose/httpbin/docker-compose.yml +++ b/internal/suites/example/compose/httpbin/docker-compose.yml @@ -4,3 +4,20 @@ services: image: citizenstig/httpbin networks: - authelianet + labels: + # Traefik 1.x + - 'traefik.frontend.rule=Host:public.example.com;Path:/headers' + - 'traefik.frontend.priority=120' + - 'traefik.frontend.auth.forward.address=https://authelia-backend:9091/api/verify?rd=https://login.example.com:8080/' + - 'traefik.frontend.auth.forward.tls.insecureSkipVerify=true' + - 'traefik.frontend.auth.forward.trustForwardHeader=true' + - 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups' + # Traefik 2.x + - 'traefik.http.routers.httpbin.rule=Host(`public.example.com`) && Path(`/headers`)' + - 'traefik.http.routers.httpbin.priority=150' + - 'traefik.http.routers.httpbin.tls=true' + - 'traefik.http.routers.httpbin.middlewares=authelia' + - 'traefik.http.middlewares.authelia.forwardauth.address=https://authelia-backend:9091/api/verify?rd=https://login.example.com:8080/' + - 'traefik.http.middlewares.authelia.forwardauth.tls.insecureSkipVerify=true' + - 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true' + - 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User, Remote-Groups' diff --git a/internal/suites/example/compose/nginx/portal/nginx.conf b/internal/suites/example/compose/nginx/portal/nginx.conf index 563d2452c..c4d107203 100644 --- a/internal/suites/example/compose/nginx/portal/nginx.conf +++ b/internal/suites/example/compose/nginx/portal/nginx.conf @@ -167,10 +167,10 @@ http { auth_request /auth_verify; auth_request_set $user $upstream_http_remote_user; - proxy_set_header Custom-Forwarded-User $user; + proxy_set_header Remote-User $user; auth_request_set $groups $upstream_http_remote_groups; - proxy_set_header Custom-Forwarded-Groups $groups; + proxy_set_header Remote-Groups $groups; set $target_url $scheme://$http_host$request_uri; error_page 401 =302 https://login.example.com:8080/?rd=$target_url; diff --git a/internal/suites/scenario_custom_headers_test.go b/internal/suites/scenario_custom_headers_test.go index 0981e27e2..57a6306e9 100644 --- a/internal/suites/scenario_custom_headers_test.go +++ b/internal/suites/scenario_custom_headers_test.go @@ -60,12 +60,17 @@ func (s *CustomHeadersScenario) TestShouldNotForwardCustomHeaderForUnauthenticat body, err := s.WebDriver().FindElement(selenium.ByTagName, "body") s.Assert().NoError(err) - s.WaitElementTextContains(ctx, s.T(), body, "httpbin:8000") + s.WaitElementTextContains(ctx, s.T(), body, "\"Host\"") + + b, err := body.Text() + s.Assert().NoError(err) + s.Assert().NotContains(b, "john") + s.Assert().NotContains(b, "admins") } type Headers struct { - ForwardedGroups string `json:"Custom-Forwarded-Groups"` - ForwardedUser string `json:"Custom-Forwarded-User"` + ForwardedGroups string `json:"Remote-Groups"` + ForwardedUser string `json:"Remote-User"` } type HeadersPayload struct { diff --git a/internal/suites/suite_haproxy.go b/internal/suites/suite_haproxy.go index 4fe94f01e..94a4460af 100644 --- a/internal/suites/suite_haproxy.go +++ b/internal/suites/suite_haproxy.go @@ -16,6 +16,7 @@ func init() { "internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/haproxy/docker-compose.yml", "internal/suites/example/compose/smtp/docker-compose.yml", + "internal/suites/example/compose/httpbin/docker-compose.yml", }) setup := func(suitePath string) error { diff --git a/internal/suites/suite_haproxy_test.go b/internal/suites/suite_haproxy_test.go index 800dd639f..cdc602f7e 100644 --- a/internal/suites/suite_haproxy_test.go +++ b/internal/suites/suite_haproxy_test.go @@ -22,6 +22,10 @@ func (s *HAProxySuite) TestTwoFactorScenario() { suite.Run(s.T(), NewTwoFactorScenario()) } +func (s *HAProxySuite) TestCustomHeaders() { + suite.Run(s.T(), NewCustomHeadersScenario()) +} + func TestHAProxySuite(t *testing.T) { suite.Run(t, NewHAProxySuite()) } diff --git a/internal/suites/suite_traefik.go b/internal/suites/suite_traefik.go index 99cb46067..a12e8846d 100644 --- a/internal/suites/suite_traefik.go +++ b/internal/suites/suite_traefik.go @@ -16,6 +16,7 @@ func init() { "internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/traefik/docker-compose.yml", "internal/suites/example/compose/smtp/docker-compose.yml", + "internal/suites/example/compose/httpbin/docker-compose.yml", }) setup := func(suitePath string) error { diff --git a/internal/suites/suite_traefik2.go b/internal/suites/suite_traefik2.go index 85e5ce0d3..596a6379e 100644 --- a/internal/suites/suite_traefik2.go +++ b/internal/suites/suite_traefik2.go @@ -16,6 +16,7 @@ func init() { "internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/traefik2/docker-compose.yml", "internal/suites/example/compose/smtp/docker-compose.yml", + "internal/suites/example/compose/httpbin/docker-compose.yml", }) setup := func(suitePath string) error { diff --git a/internal/suites/suite_traefik2_test.go b/internal/suites/suite_traefik2_test.go index a27bd3f65..674f48a3c 100644 --- a/internal/suites/suite_traefik2_test.go +++ b/internal/suites/suite_traefik2_test.go @@ -22,6 +22,10 @@ func (s *Traefik2Suite) TestTwoFactorScenario() { suite.Run(s.T(), NewTwoFactorScenario()) } +func (s *Traefik2Suite) TestCustomHeaders() { + suite.Run(s.T(), NewCustomHeadersScenario()) +} + func TestTraefik2Suite(t *testing.T) { suite.Run(t, NewTraefik2Suite()) } diff --git a/internal/suites/suite_traefik_test.go b/internal/suites/suite_traefik_test.go index d23b713bf..83c6625d0 100644 --- a/internal/suites/suite_traefik_test.go +++ b/internal/suites/suite_traefik_test.go @@ -26,6 +26,10 @@ func (s *TraefikSuite) TestRedirectionURLScenario() { suite.Run(s.T(), NewRedirectionURLScenario()) } +func (s *TraefikSuite) TestCustomHeaders() { + suite.Run(s.T(), NewCustomHeadersScenario()) +} + func TestTraefikSuite(t *testing.T) { suite.Run(t, NewTraefikSuite()) }