From 89f251d55947c72cc3419968065ab8b83a337c97 Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Wed, 16 Oct 2024 18:01:59 +0200 Subject: [PATCH] tests/e2e: add forward auth e2e test (#11374) * add nginx forward_auth e2e tests Signed-off-by: Jens Langhammer * add envoy Signed-off-by: Jens Langhammer * cleanup Signed-off-by: Jens Langhammer * remove even more duplicate code Signed-off-by: Jens Langhammer * cleanup more Signed-off-by: Jens Langhammer * add traefik static config Signed-off-by: Jens Langhammer * more cleanup, don't generate dex config cause they support env variables Signed-off-by: Jens Langhammer * use default dex entrypoint to use templating Signed-off-by: Jens Langhammer * remove options that are always set as default Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer * fix compose flag Signed-off-by: Jens Langhammer * add caddy Signed-off-by: Jens Langhammer * merge python files Signed-off-by: Jens Langhammer * use whoami api to check better Signed-off-by: Jens Langhammer * fix envoy config Signed-off-by: Jens Langhammer * set invalidation flow Signed-off-by: Jens Langhammer * fix logout checks Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .github/workflows/ci-main.yml | 2 +- .../proxy_forward_auth/caddy_single/Caddyfile | 21 ++ .../envoy_single/envoy.yaml | 99 ++++++++ .../nginx_single/nginx.conf | 59 +++++ .../traefik_single/config-static.yaml | 57 +++++ tests/e2e/sources_oauth2_dex/dex.yaml | 23 ++ tests/e2e/test_provider_ldap.py | 19 +- tests/e2e/test_provider_oauth2_github.py | 18 +- tests/e2e/test_provider_oauth2_grafana.py | 21 +- tests/e2e/test_provider_oidc.py | 18 +- tests/e2e/test_provider_oidc_implicit.py | 18 +- tests/e2e/test_provider_proxy.py | 52 ++-- tests/e2e/test_provider_proxy_forward.py | 227 ++++++++++++++++++ tests/e2e/test_provider_radius.py | 19 +- tests/e2e/test_provider_saml.py | 24 +- tests/e2e/test_source_ldap_samba.py | 22 +- tests/e2e/test_source_oauth_oauth1.py | 14 +- tests/e2e/test_source_oauth_oauth2.py | 71 ++---- tests/e2e/test_source_saml.py | 19 +- tests/e2e/test_source_scim.py | 15 +- tests/e2e/utils.py | 120 +++++---- tests/integration/test_outpost_docker.py | 20 +- tests/integration/test_proxy_docker.py | 20 +- .../providers/proxy/_traefik_standalone.md | 2 +- 24 files changed, 678 insertions(+), 302 deletions(-) create mode 100644 tests/e2e/proxy_forward_auth/caddy_single/Caddyfile create mode 100644 tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml create mode 100644 tests/e2e/proxy_forward_auth/nginx_single/nginx.conf create mode 100644 tests/e2e/proxy_forward_auth/traefik_single/config-static.yaml create mode 100644 tests/e2e/sources_oauth2_dex/dex.yaml create mode 100644 tests/e2e/test_provider_proxy_forward.py diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index d19c1f910eae..4812a57a033c 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -180,7 +180,7 @@ jobs: uses: ./.github/actions/setup - name: Setup e2e env (chrome, etc) run: | - docker compose -f tests/e2e/docker-compose.yml up -d + docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull - id: cache-web uses: actions/cache@v4 with: diff --git a/tests/e2e/proxy_forward_auth/caddy_single/Caddyfile b/tests/e2e/proxy_forward_auth/caddy_single/Caddyfile new file mode 100644 index 000000000000..741ccee4d5e0 --- /dev/null +++ b/tests/e2e/proxy_forward_auth/caddy_single/Caddyfile @@ -0,0 +1,21 @@ +http://localhost { + # directive execution order is only as stated if enclosed with route. + route { + # always forward outpost path to actual outpost + reverse_proxy /outpost.goauthentik.io/* http://ak-test-outpost:9000 + + # forward authentication to outpost + forward_auth http://ak-test-outpost:9000 { + uri /outpost.goauthentik.io/auth/caddy + + # capitalization of the headers is important, otherwise they will be empty + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + + # optional, in this config trust all private ranges, should probably be set to the outposts IP + trusted_proxies private_ranges + } + + # actual site configuration below, for example + reverse_proxy ak-whoami + } +} diff --git a/tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml b/tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml new file mode 100644 index 000000000000..1ceb09c846d2 --- /dev/null +++ b/tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml @@ -0,0 +1,99 @@ +# yaml-language-server: $schema=https://github.com/jcchavezs/envoy-config-schema/releases/download/v1.21.0/v3_Bootstrap.json +static_resources: + listeners: + - name: main_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + upgrade_configs: + - upgrade_type: websocket + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + http_filters: + - name: envoy.filters.http.ext_authz + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + transport_api_version: V3 + http_service: + path_prefix: /outpost.goauthentik.io/auth/envoy + server_uri: + uri: http://ak-test-outpost:9000 + cluster: authentik_outpost + timeout: 0.25s + authorization_request: + allowed_headers: + patterns: + - exact: "cookie" + ignore_case: true + authorization_response: + allowed_upstream_headers: + patterns: + - exact: "set-cookie" + ignore_case: true + - prefix: "x-authentik-" + ignore_case: true + allowed_client_headers_on_success: + patterns: + - exact: "cookie" + ignore_case: true + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["localhost"] + routes: + - match: + prefix: "/outpost.goauthentik.io" + route: + cluster: authentik_outpost + - match: + prefix: "/" + route: + cluster: whoami + - name: local_service + domains: ["*"] + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + routes: + - match: + prefix: "/" + route: + cluster: authentik_outpost + + clusters: + - name: authentik_outpost + type: LOGICAL_DNS + load_assignment: + cluster_name: authentik_outpost + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ak-test-outpost + port_value: 9000 + - name: whoami + type: LOGICAL_DNS + load_assignment: + cluster_name: whoami + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ak-whoami + port_value: 80 diff --git a/tests/e2e/proxy_forward_auth/nginx_single/nginx.conf b/tests/e2e/proxy_forward_auth/nginx_single/nginx.conf new file mode 100644 index 000000000000..50a34064a48c --- /dev/null +++ b/tests/e2e/proxy_forward_auth/nginx_single/nginx.conf @@ -0,0 +1,59 @@ +server { + listen 80; + server_name _; + + # Increase buffer size for large headers + # This is needed only if you get 'upstream sent too big header while reading response + # header from upstream' error when trying to access an application protected by goauthentik + proxy_buffers 8 16k; + proxy_buffer_size 32k; + + location / { + proxy_pass http://ak-whoami; + proxy_set_header Host $host; + + ############################## + # authentik-specific config + ############################## + auth_request /outpost.goauthentik.io/auth/nginx; + error_page 401 = @goauthentik_proxy_signin; + auth_request_set $auth_cookie $upstream_http_set_cookie; + add_header Set-Cookie $auth_cookie; + + # translate headers from the outposts back to the actual upstream + auth_request_set $authentik_username $upstream_http_x_authentik_username; + auth_request_set $authentik_groups $upstream_http_x_authentik_groups; + auth_request_set $authentik_email $upstream_http_x_authentik_email; + auth_request_set $authentik_name $upstream_http_x_authentik_name; + auth_request_set $authentik_uid $upstream_http_x_authentik_uid; + + proxy_set_header X-authentik-username $authentik_username; + proxy_set_header X-authentik-groups $authentik_groups; + proxy_set_header X-authentik-email $authentik_email; + proxy_set_header X-authentik-name $authentik_name; + proxy_set_header X-authentik-uid $authentik_uid; + } + + # all requests to /outpost.goauthentik.io must be accessible without authentication + location /outpost.goauthentik.io { + proxy_pass http://ak-test-outpost:9000/outpost.goauthentik.io; + # ensure the host of this vserver matches your external URL you've configured + # in authentik + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + add_header Set-Cookie $auth_cookie; + auth_request_set $auth_cookie $upstream_http_set_cookie; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + + # Special location for when the /auth endpoint returns a 401, + # redirect to the /start URL which initiates SSO + location @goauthentik_proxy_signin { + internal; + add_header Set-Cookie $auth_cookie; + return 302 /outpost.goauthentik.io/start?rd=$request_uri; + # For domain level, use the below error_page to redirect to your authentik server with the full redirect path + # return 302 https://localhost/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri; + } +} diff --git a/tests/e2e/proxy_forward_auth/traefik_single/config-static.yaml b/tests/e2e/proxy_forward_auth/traefik_single/config-static.yaml new file mode 100644 index 000000000000..3d87d4f2ebdd --- /dev/null +++ b/tests/e2e/proxy_forward_auth/traefik_single/config-static.yaml @@ -0,0 +1,57 @@ +# yaml-language-server: $schema=https://json.schemastore.org/traefik-v2.json +api: + insecure: true + debug: true + +log: + level: debug +accessLog: + filePath: /dev/stdout + +entryPoints: + web: + address: ":80" + +# Re-use the same config file to define everything +providers: + file: + filename: /etc/traefik/traefik.yml + +http: + middlewares: + authentik: + forwardAuth: + address: http://ak-test-outpost:9000/outpost.goauthentik.io/auth/traefik + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-email + - X-authentik-name + - X-authentik-uid + - X-authentik-jwt + - X-authentik-meta-jwks + - X-authentik-meta-outpost + - X-authentik-meta-provider + - X-authentik-meta-app + - X-authentik-meta-version + routers: + default-router: + rule: "Host(`localhost`)" + middlewares: + - authentik + priority: 10 + service: app + default-router-auth: + rule: "Host(`localhost`) && PathPrefix(`/outpost.goauthentik.io/`)" + priority: 15 + service: authentik + services: + app: + loadBalancer: + servers: + - url: http://ak-whoami + authentik: + loadBalancer: + servers: + - url: http://ak-test-outpost:9000/outpost.goauthentik.io diff --git a/tests/e2e/sources_oauth2_dex/dex.yaml b/tests/e2e/sources_oauth2_dex/dex.yaml new file mode 100644 index 000000000000..a8ecb617f33d --- /dev/null +++ b/tests/e2e/sources_oauth2_dex/dex.yaml @@ -0,0 +1,23 @@ +--- +enablePasswordDB: true +issuer: http://127.0.0.1:5556/dex +logger: + level: debug +staticClients: + - id: example-app + name: Example App + redirectURIs: + - {{ .Env.AK_REDIRECT_URL }} + secret: {{ .Env.AK_CLIENT_SECRET }} +staticPasswords: + - email: admin@example.com + # hash for 'password', for testing + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + username: admin +storage: + config: + file: "/tmp/dex.db" + type: sqlite3 +web: + http: 0.0.0.0:5556 diff --git a/tests/e2e/test_provider_ldap.py b/tests/e2e/test_provider_ldap.py index 5d521980789b..4eb163797c7a 100644 --- a/tests/e2e/test_provider_ldap.py +++ b/tests/e2e/test_provider_ldap.py @@ -3,8 +3,6 @@ from dataclasses import asdict from time import sleep -from docker.client import DockerClient, from_env -from docker.models.containers import Container from guardian.shortcuts import assign_perm from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server from ldap3.core.exceptions import LDAPInvalidCredentialsResult @@ -24,29 +22,18 @@ class TestProviderLDAP(SeleniumTestCase): """LDAP and Outpost e2e tests""" - ldap_container: Container - - def tearDown(self) -> None: - super().tearDown() - self.output_container_logs(self.ldap_container) - self.ldap_container.kill() - - def start_ldap(self, outpost: Outpost) -> Container: + def start_ldap(self, outpost: Outpost): """Start ldap container based on outpost created""" - client: DockerClient = from_env() - container = client.containers.run( + self.run_container( image=self.get_container_image("ghcr.io/goauthentik/dev-ldap"), - detach=True, ports={ "3389": "3389", "6636": "6636", }, environment={ - "AUTHENTIK_HOST": self.live_server_url, "AUTHENTIK_TOKEN": outpost.token.key, }, ) - return container def _prepare(self) -> User: """prepare user, provider, app and container""" @@ -68,7 +55,7 @@ def _prepare(self) -> User: ) outpost.providers.add(ldap) - self.ldap_container = self.start_ldap(outpost) + self.start_ldap(outpost) # Wait until outpost healthcheck succeeds healthcheck_retries = 0 diff --git a/tests/e2e/test_provider_oauth2_github.py b/tests/e2e/test_provider_oauth2_github.py index 64eecd032d9c..31db3b04c7d7 100644 --- a/tests/e2e/test_provider_oauth2_github.py +++ b/tests/e2e/test_provider_oauth2_github.py @@ -1,7 +1,6 @@ """test OAuth Provider flow""" from time import sleep -from typing import Any from docker.types import Healthcheck from selenium.webdriver.common.by import By @@ -24,22 +23,17 @@ def setUp(self): self.client_id = generate_id() self.client_secret = generate_key() super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - """Setup client grafana container which we test OAuth against""" - return { - "image": "grafana/grafana:7.1.0", - "detach": True, - "ports": { + self.run_container( + image="grafana/grafana:7.1.0", + ports={ "3000": "3000", }, - "auto_remove": True, - "healthcheck": Healthcheck( + healthcheck=Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:3000"], interval=5 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000, ), - "environment": { + environment={ "GF_AUTH_GITHUB_ENABLED": "true", "GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true", "GF_AUTH_GITHUB_CLIENT_ID": self.client_id, @@ -54,7 +48,7 @@ def get_container_specs(self) -> dict[str, Any] | None: "GF_AUTH_GITHUB_API_URL": self.url("authentik_providers_oauth2_root:github-user"), "GF_LOG_LEVEL": "debug", }, - } + ) @retry() @apply_blueprint( diff --git a/tests/e2e/test_provider_oauth2_grafana.py b/tests/e2e/test_provider_oauth2_grafana.py index ded28f59585c..1a862b3246f0 100644 --- a/tests/e2e/test_provider_oauth2_grafana.py +++ b/tests/e2e/test_provider_oauth2_grafana.py @@ -1,7 +1,6 @@ """test OAuth2 OpenID Provider flow""" from time import sleep -from typing import Any from docker.types import Healthcheck from selenium.webdriver.common.by import By @@ -32,21 +31,17 @@ def setUp(self): self.client_secret = generate_key() self.app_slug = generate_id(20) super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "grafana/grafana:7.1.0", - "detach": True, - "auto_remove": True, - "healthcheck": Healthcheck( + self.run_container( + image="grafana/grafana:7.1.0", + ports={ + "3000": "3000", + }, + healthcheck=Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:3000"], interval=5 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000, ), - "ports": { - "3000": "3000", - }, - "environment": { + environment={ "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, @@ -60,7 +55,7 @@ def get_container_specs(self) -> dict[str, Any] | None: ), "GF_LOG_LEVEL": "debug", }, - } + ) @retry() @apply_blueprint( diff --git a/tests/e2e/test_provider_oidc.py b/tests/e2e/test_provider_oidc.py index 5af7e3f2a955..0bccb12f58f3 100644 --- a/tests/e2e/test_provider_oidc.py +++ b/tests/e2e/test_provider_oidc.py @@ -3,8 +3,6 @@ from json import loads from time import sleep -from docker import DockerClient, from_env -from docker.models.containers import Container from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec @@ -34,13 +32,11 @@ def setUp(self): self.application_slug = generate_id() super().setUp() - def setup_client(self) -> Container: + def setup_client(self): """Setup client oidc-test-client container which we test OIDC against""" sleep(1) - client: DockerClient = from_env() - container = client.containers.run( + self.run_container( image="ghcr.io/beryju/oidc-test-client:2.1", - detach=True, ports={ "9009": "9009", }, @@ -50,8 +46,6 @@ def setup_client(self) -> Container: "OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/", }, ) - self.wait_for_container(container) - return container @retry() @apply_blueprint( @@ -91,7 +85,7 @@ def test_redirect_uri_error(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009") sleep(2) @@ -140,7 +134,7 @@ def test_authorization_consent_implied(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009") self.login() @@ -210,7 +204,7 @@ def test_authorization_consent_explicit(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009") self.login() @@ -287,7 +281,7 @@ def test_authorization_denied(self): ) PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009") self.login() self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))) diff --git a/tests/e2e/test_provider_oidc_implicit.py b/tests/e2e/test_provider_oidc_implicit.py index 9d35c2cc4357..7d1a0497fecd 100644 --- a/tests/e2e/test_provider_oidc_implicit.py +++ b/tests/e2e/test_provider_oidc_implicit.py @@ -3,8 +3,6 @@ from json import loads from time import sleep -from docker import DockerClient, from_env -from docker.models.containers import Container from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec @@ -34,13 +32,11 @@ def setUp(self): self.application_slug = "test" super().setUp() - def setup_client(self) -> Container: + def setup_client(self): """Setup client oidc-test-client container which we test OIDC against""" sleep(1) - client: DockerClient = from_env() - container = client.containers.run( + self.run_container( image="ghcr.io/beryju/oidc-test-client:2.1", - detach=True, ports={ "9009": "9009", }, @@ -50,8 +46,6 @@ def setup_client(self) -> Container: "OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/", }, ) - self.wait_for_container(container) - return container @retry() @apply_blueprint( @@ -93,7 +87,7 @@ def test_redirect_uri_error(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009/implicit/") sleep(2) @@ -142,7 +136,7 @@ def test_authorization_consent_implied(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009/implicit/") self.wait.until(ec.title_contains("authentik")) @@ -194,7 +188,7 @@ def test_authorization_consent_explicit(self): slug=self.application_slug, provider=provider, ) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009/implicit/") self.wait.until(ec.title_contains("authentik")) @@ -268,7 +262,7 @@ def test_authorization_denied(self): ) PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) - self.container = self.setup_client() + self.setup_client() self.driver.get("http://localhost:9009/implicit/") self.wait.until(ec.title_contains("authentik")) self.login() diff --git a/tests/e2e/test_provider_proxy.py b/tests/e2e/test_provider_proxy.py index 30c706007138..90c55781d0c8 100644 --- a/tests/e2e/test_provider_proxy.py +++ b/tests/e2e/test_provider_proxy.py @@ -2,14 +2,12 @@ from base64 import b64encode from dataclasses import asdict +from json import loads from sys import platform from time import sleep -from typing import Any from unittest.case import skip, skipUnless from channels.testing import ChannelsLiveServerTestCase -from docker.client import DockerClient, from_env -from docker.models.containers import Container from selenium.webdriver.common.by import By from authentik.blueprints.tests import apply_blueprint, reconcile_app @@ -25,38 +23,26 @@ class TestProviderProxy(SeleniumTestCase): """Proxy and Outpost e2e tests""" - proxy_container: Container - - def tearDown(self) -> None: - super().tearDown() - self.output_container_logs(self.proxy_container) - self.proxy_container.kill() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "traefik/whoami:latest", - "detach": True, - "ports": { + def setUp(self): + super().setUp() + self.run_container( + image="traefik/whoami:latest", + ports={ "80": "80", }, - "auto_remove": True, - } + ) - def start_proxy(self, outpost: Outpost) -> Container: + def start_proxy(self, outpost: Outpost): """Start proxy container based on outpost created""" - client: DockerClient = from_env() - container = client.containers.run( + self.run_container( image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), - detach=True, ports={ "9000": "9000", }, environment={ - "AUTHENTIK_HOST": self.live_server_url, "AUTHENTIK_TOKEN": outpost.token.key, }, ) - return container @retry() @apply_blueprint( @@ -99,7 +85,7 @@ def test_proxy_simple(self): outpost.providers.add(proxy) outpost.build_user_permissions(outpost.user) - self.proxy_container = self.start_proxy(outpost) + self.start_proxy(outpost) # Wait until outpost healthcheck succeeds healthcheck_retries = 0 @@ -112,13 +98,15 @@ def test_proxy_simple(self): sleep(0.5) sleep(5) - self.driver.get("http://localhost:9000") + self.driver.get("http://localhost:9000/api") self.login() sleep(1) full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text - self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text) - self.assertIn("X-Foo: bar", full_body_text) + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) + self.assertEqual(body["headers"]["X-Foo"], ["bar"]) self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") sleep(2) @@ -173,7 +161,7 @@ def test_proxy_basic_auth(self): outpost.providers.add(proxy) outpost.build_user_permissions(outpost.user) - self.proxy_container = self.start_proxy(outpost) + self.start_proxy(outpost) # Wait until outpost healthcheck succeeds healthcheck_retries = 0 @@ -186,14 +174,16 @@ def test_proxy_basic_auth(self): sleep(0.5) sleep(5) - self.driver.get("http://localhost:9000") + self.driver.get("http://localhost:9000/api") self.login() sleep(1) full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text - self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text) + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) auth_header = b64encode(f"{cred}:{cred}".encode()).decode() - self.assertIn(f"Authorization: Basic {auth_header}", full_body_text) + self.assertEqual(body["headers"]["Authorization"], [f"Basic {auth_header}"]) self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") sleep(2) diff --git a/tests/e2e/test_provider_proxy_forward.py b/tests/e2e/test_provider_proxy_forward.py new file mode 100644 index 000000000000..e1416878660e --- /dev/null +++ b/tests/e2e/test_provider_proxy_forward.py @@ -0,0 +1,227 @@ +"""Proxy and Outpost e2e tests""" + +from json import loads +from pathlib import Path +from time import sleep + +from selenium.webdriver.common.by import By + +from authentik.blueprints.tests import apply_blueprint, reconcile_app +from authentik.core.models import Application +from authentik.flows.models import Flow +from authentik.lib.generators import generate_id +from authentik.outposts.models import Outpost, OutpostType +from authentik.providers.proxy.models import ProxyMode, ProxyProvider +from tests.e2e.utils import SeleniumTestCase, retry + + +class TestProviderProxyForward(SeleniumTestCase): + """Proxy and Outpost e2e tests""" + + def setUp(self): + super().setUp() + self.run_container( + image="traefik/whoami:latest", + name="ak-whoami", + ) + + def start_outpost(self, outpost: Outpost): + """Start proxy container based on outpost created""" + self.run_container( + image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), + ports={ + "9000": "9000", + }, + environment={ + "AUTHENTIK_TOKEN": outpost.token.key, + }, + name="ak-test-outpost", + ) + + @apply_blueprint( + "default/flow-default-authentication-flow.yaml", + "default/flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "default/flow-default-provider-authorization-implicit-consent.yaml", + "default/flow-default-provider-invalidation.yaml", + ) + @apply_blueprint( + "system/providers-oauth2.yaml", + "system/providers-proxy.yaml", + ) + @reconcile_app("authentik_crypto") + def prepare(self): + proxy: ProxyProvider = ProxyProvider.objects.create( + name=generate_id(), + mode=ProxyMode.FORWARD_SINGLE, + authorization_flow=Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ), + invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"), + internal_host=f"http://{self.host}", + external_host="http://localhost", + ) + # Ensure OAuth2 Params are set + proxy.set_oauth_defaults() + proxy.save() + # we need to create an application to actually access the proxy + Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy) + outpost: Outpost = Outpost.objects.create( + name=generate_id(), + type=OutpostType.PROXY, + ) + outpost.providers.add(proxy) + outpost.build_user_permissions(outpost.user) + + self.start_outpost(outpost) + + # Wait until outpost healthcheck succeeds + healthcheck_retries = 0 + while healthcheck_retries < 50: # noqa: PLR2004 + if len(outpost.state) > 0: + state = outpost.state[0] + if state.last_seen: + break + healthcheck_retries += 1 + sleep(0.5) + sleep(5) + + @retry() + def test_traefik(self): + """Test traefik""" + local_config_path = ( + Path(__file__).parent / "proxy_forward_auth" / "traefik_single" / "config-static.yaml" + ) + self.run_container( + image="docker.io/library/traefik:3.1", + ports={ + "80": "80", + }, + volumes={ + local_config_path: { + "bind": "/etc/traefik/traefik.yml", + } + }, + ) + + self.prepare() + + self.driver.get("http://localhost/api") + self.login() + sleep(1) + + full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) + + self.driver.get("http://localhost/outpost.goauthentik.io/sign_out") + sleep(2) + flow_executor = self.get_shadow_root("ak-flow-executor") + session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor) + title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text + self.assertIn("You've logged out of", title) + + @retry() + def test_nginx(self): + """Test nginx""" + self.prepare() + + # Start nginx last so all hosts are resolvable, otherwise nginx exits + self.run_container( + image="docker.io/library/nginx:1.27", + ports={ + "80": "80", + }, + volumes={ + f"{Path(__file__).parent / "proxy_forward_auth" / "nginx_single" / "nginx.conf"}": { + "bind": "/etc/nginx/conf.d/default.conf", + } + }, + ) + + self.driver.get("http://localhost/api") + self.login() + sleep(1) + + full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) + + self.driver.get("http://localhost/outpost.goauthentik.io/sign_out") + sleep(2) + flow_executor = self.get_shadow_root("ak-flow-executor") + session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor) + title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text + self.assertIn("You've logged out of", title) + + @retry() + def test_envoy(self): + """Test envoy""" + self.run_container( + image="docker.io/envoyproxy/envoy:v1.25-latest", + ports={ + "10000": "80", + }, + volumes={ + f"{Path(__file__).parent / "proxy_forward_auth" / "envoy_single" / "envoy.yaml"}": { + "bind": "/etc/envoy/envoy.yaml", + } + }, + ) + + self.prepare() + + self.driver.get("http://localhost/api") + self.login() + sleep(1) + + full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) + + self.driver.get("http://localhost/outpost.goauthentik.io/sign_out") + sleep(2) + flow_executor = self.get_shadow_root("ak-flow-executor") + session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor) + title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text + self.assertIn("You've logged out of", title) + + @retry() + def test_caddy(self): + """Test caddy""" + local_config_path = ( + Path(__file__).parent / "proxy_forward_auth" / "caddy_single" / "Caddyfile" + ) + self.run_container( + image="docker.io/library/caddy:2.8", + ports={ + "80": "80", + }, + volumes={ + local_config_path: { + "bind": "/etc/caddy/Caddyfile", + } + }, + ) + + self.prepare() + + self.driver.get("http://localhost/api") + self.login() + sleep(1) + + full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text + body = loads(full_body_text) + + self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) + + self.driver.get("http://localhost/outpost.goauthentik.io/sign_out") + sleep(2) + flow_executor = self.get_shadow_root("ak-flow-executor") + session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor) + title = session_end_stage.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text + self.assertIn("You've logged out of", title) diff --git a/tests/e2e/test_provider_radius.py b/tests/e2e/test_provider_radius.py index cbe3fbffeaf0..0392a7737266 100644 --- a/tests/e2e/test_provider_radius.py +++ b/tests/e2e/test_provider_radius.py @@ -3,8 +3,6 @@ from dataclasses import asdict from time import sleep -from docker.client import DockerClient, from_env -from docker.models.containers import Container from pyrad.client import Client from pyrad.dictionary import Dictionary from pyrad.packet import AccessAccept, AccessReject, AccessRequest @@ -21,30 +19,19 @@ class TestProviderRadius(SeleniumTestCase): """Radius Outpost e2e tests""" - radius_container: Container - def setUp(self): super().setUp() self.shared_secret = generate_key() - def tearDown(self) -> None: - super().tearDown() - self.output_container_logs(self.radius_container) - self.radius_container.kill() - - def start_radius(self, outpost: Outpost) -> Container: + def start_radius(self, outpost: Outpost): """Start radius container based on outpost created""" - client: DockerClient = from_env() - container = client.containers.run( + self.run_container( image=self.get_container_image("ghcr.io/goauthentik/dev-radius"), - detach=True, ports={"1812/udp": "1812/udp"}, environment={ - "AUTHENTIK_HOST": self.live_server_url, "AUTHENTIK_TOKEN": outpost.token.key, }, ) - return container def _prepare(self) -> User: """prepare user, provider, app and container""" @@ -62,7 +49,7 @@ def _prepare(self) -> User: ) outpost.providers.add(radius) - self.radius_container = self.start_radius(outpost) + self.start_radius(outpost) # Wait until outpost healthcheck succeeds healthcheck_retries = 0 diff --git a/tests/e2e/test_provider_saml.py b/tests/e2e/test_provider_saml.py index 170353741ef4..abca11444d36 100644 --- a/tests/e2e/test_provider_saml.py +++ b/tests/e2e/test_provider_saml.py @@ -3,8 +3,6 @@ from json import loads from time import sleep -from docker import DockerClient, from_env -from docker.models.containers import Container from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec @@ -22,11 +20,8 @@ class TestProviderSAML(SeleniumTestCase): """test SAML Provider flow""" - container: Container - - def setup_client(self, provider: SAMLProvider, force_post: bool = False) -> Container: + def setup_client(self, provider: SAMLProvider, force_post: bool = False): """Setup client saml-sp container which we test SAML against""" - client: DockerClient = from_env() metadata_url = ( self.url( "authentik_api:samlprovider-metadata", @@ -36,9 +31,8 @@ def setup_client(self, provider: SAMLProvider, force_post: bool = False) -> Cont ) if force_post: metadata_url += f"&force_binding={SAML_BINDING_POST}" - container = client.containers.run( + self.run_container( image="ghcr.io/beryju/saml-test-sp:1.1", - detach=True, ports={ "9009": "9009", }, @@ -48,8 +42,6 @@ def setup_client(self, provider: SAMLProvider, force_post: bool = False) -> Cont "SP_METADATA_URL": metadata_url, }, ) - self.wait_for_container(container) - return container @retry() @apply_blueprint( @@ -85,7 +77,7 @@ def test_sp_initiated_implicit(self): slug="authentik-saml", provider=provider, ) - self.container = self.setup_client(provider) + self.setup_client(provider) self.driver.get("http://localhost:9009") self.login() self.wait_for_url("http://localhost:9009/") @@ -153,7 +145,7 @@ def test_sp_initiated_explicit(self): slug="authentik-saml", provider=provider, ) - self.container = self.setup_client(provider) + self.setup_client(provider) self.driver.get("http://localhost:9009") self.login() @@ -236,7 +228,7 @@ def test_sp_initiated_explicit_post(self): slug="authentik-saml", provider=provider, ) - self.container = self.setup_client(provider, True) + self.setup_client(provider, True) self.driver.get("http://localhost:9009") self.login() @@ -319,7 +311,7 @@ def test_idp_initiated_implicit(self): slug="authentik-saml", provider=provider, ) - self.container = self.setup_client(provider) + self.setup_client(provider) self.driver.get( self.url( "authentik_providers_saml:sso-init", @@ -397,7 +389,7 @@ def test_sp_initiated_denied(self): provider=provider, ) PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) - self.container = self.setup_client(provider) + self.setup_client(provider) self.driver.get("http://localhost:9009/") self.login() @@ -444,7 +436,7 @@ def test_slo(self): slug="authentik-saml", provider=provider, ) - self.container = self.setup_client(provider) + self.setup_client(provider) self.driver.get("http://localhost:9009") self.login() self.wait_for_url("http://localhost:9009/") diff --git a/tests/e2e/test_source_ldap_samba.py b/tests/e2e/test_source_ldap_samba.py index d66687bace49..59a9584b7258 100644 --- a/tests/e2e/test_source_ldap_samba.py +++ b/tests/e2e/test_source_ldap_samba.py @@ -1,7 +1,5 @@ """test LDAP Source""" -from typing import Any - from django.db.models import Q from ldap3.core.exceptions import LDAPSessionTerminatedByServerError @@ -22,22 +20,18 @@ class TestSourceLDAPSamba(SeleniumTestCase): def setUp(self): self.admin_password = generate_key() super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "ghcr.io/beryju/test-samba-dc:latest", - "detach": True, - "cap_add": ["SYS_ADMIN"], - "ports": { + self.samba = self.run_container( + image="ghcr.io/beryju/test-samba-dc:latest", + cap_add=["SYS_ADMIN"], + ports={ "389": "389/tcp", }, - "auto_remove": True, - "environment": { + environment={ "SMB_DOMAIN": "test.goauthentik.io", "SMB_NETBIOS": "goauthentik", "SMB_ADMIN_PASSWORD": self.admin_password, }, - } + ) @retry(exceptions=[LDAPSessionTerminatedByServerError]) @apply_blueprint( @@ -148,7 +142,7 @@ def test_sync_password(self): UserLDAPSynchronizer(source).sync_full() username = "bob" password = generate_id() - result = self.container.exec_run( + result = self.samba.exec_run( ["samba-tool", "user", "setpassword", username, "--newpassword", password] ) self.assertEqual(result.exit_code, 0) @@ -163,7 +157,7 @@ def test_sync_password(self): self.assertTrue(user.check_password(password)) # Set new password new_password = generate_id() - result = self.container.exec_run( + result = self.samba.exec_run( ["samba-tool", "user", "setpassword", username, "--newpassword", new_password] ) self.assertEqual(result.exit_code, 0) diff --git a/tests/e2e/test_source_oauth_oauth1.py b/tests/e2e/test_source_oauth_oauth1.py index 9ebc02d8119b..6236d1a3742a 100644 --- a/tests/e2e/test_source_oauth_oauth1.py +++ b/tests/e2e/test_source_oauth_oauth1.py @@ -56,14 +56,10 @@ def setUp(self) -> None: self.client_secret = generate_key() self.source_slug = generate_id() super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "ghcr.io/beryju/oauth1-test-server:v1.1", - "detach": True, - "ports": {"5000": "5001"}, - "auto_remove": True, - "environment": { + self.run_container( + image="ghcr.io/beryju/oauth1-test-server:v1.1", + ports={"5000": "5001"}, + environment={ "OAUTH1_CLIENT_ID": self.client_id, "OAUTH1_CLIENT_SECRET": self.client_secret, "OAUTH1_REDIRECT_URI": self.url( @@ -71,7 +67,7 @@ def get_container_specs(self) -> dict[str, Any] | None: source_slug=self.source_slug, ), }, - } + ) def create_objects(self): """Create required objects""" diff --git a/tests/e2e/test_source_oauth_oauth2.py b/tests/e2e/test_source_oauth_oauth2.py index 55b363feba4d..15bbcb995a1a 100644 --- a/tests/e2e/test_source_oauth_oauth2.py +++ b/tests/e2e/test_source_oauth_oauth2.py @@ -3,15 +3,12 @@ from json import loads from pathlib import Path from time import sleep -from typing import Any -from docker.models.containers import Container from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.wait import WebDriverWait -from yaml import safe_dump from authentik.blueprints.tests import apply_blueprint from authentik.core.models import User @@ -21,69 +18,35 @@ from authentik.stages.identification.models import IdentificationStage from tests.e2e.utils import SeleniumTestCase, retry -CONFIG_PATH = "/tmp/dex.yml" # nosec - class TestSourceOAuth2(SeleniumTestCase): """test OAuth Source flow""" - container: Container - def setUp(self): self.client_secret = generate_key() self.slug = generate_id() - self.prepare_dex_config() super().setUp() - - def prepare_dex_config(self): - """Since Dex does not document which environment - variables can be used to configure clients""" - config = { - "enablePasswordDB": True, - "issuer": "http://127.0.0.1:5556/dex", - "logger": {"level": "debug"}, - "staticClients": [ - { - "id": "example-app", - "name": "Example App", - "redirectURIs": [ - self.url( - "authentik_sources_oauth:oauth-client-callback", - source_slug=self.slug, - ) - ], - "secret": self.client_secret, - } - ], - "staticPasswords": [ - { - "email": "admin@example.com", - # hash for password - "hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W", - "userID": "08a8684b-db88-4b73-90a9-3cd1661f5466", - "username": "admin", - } - ], - "storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"}, # nosec - "web": {"http": "0.0.0.0:5556"}, - } - with open(CONFIG_PATH, "w+", encoding="utf8") as _file: - safe_dump(config, _file) - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "ghcr.io/dexidp/dex:v2.28.1", - "detach": True, - "ports": {"5556": "5556"}, - "auto_remove": True, - "command": "dex serve /config.yml", - "healthcheck": Healthcheck( + self.run_container( + image="ghcr.io/dexidp/dex:v2.28.1", + ports={"5556": "5556"}, + healthcheck=Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"], interval=5 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000, ), - "volumes": {str(Path(CONFIG_PATH).absolute()): {"bind": "/config.yml", "mode": "ro"}}, - } + environment={ + "AK_REDIRECT_URL": self.url( + "authentik_sources_oauth:oauth-client-callback", + source_slug=self.slug, + ), + "AK_CLIENT_SECRET": self.client_secret, + }, + volumes={ + f"{Path(__file__).parent / "sources_oauth2_dex" / "dex.yaml"}": { + "bind": "/etc/dex/config.docker.yaml", + } + }, + ) def create_objects(self): """Create required objects""" diff --git a/tests/e2e/test_source_saml.py b/tests/e2e/test_source_saml.py index 4f7c26019717..da01fd2ab23a 100644 --- a/tests/e2e/test_source_saml.py +++ b/tests/e2e/test_source_saml.py @@ -2,7 +2,6 @@ from pathlib import Path from time import sleep -from typing import Any from docker.types import Healthcheck from selenium.webdriver.common.by import By @@ -77,19 +76,15 @@ class TestSourceSAML(SeleniumTestCase): def setUp(self): self.slug = generate_id() super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": "kristophjunge/test-saml-idp:1.15", - "detach": True, - "ports": {"8080": "8080"}, - "auto_remove": True, - "healthcheck": Healthcheck( + self.run_container( + image="kristophjunge/test-saml-idp:1.15", + ports={"8080": "8080"}, + healthcheck=Healthcheck( test=["CMD", "curl", "http://localhost:8080"], interval=5 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000, ), - "volumes": { + volumes={ str( (Path(__file__).parent / Path("test-saml-idp/saml20-sp-remote.php")).absolute() ): { @@ -97,7 +92,7 @@ def get_container_specs(self) -> dict[str, Any] | None: "mode": "ro", } }, - "environment": { + environment={ "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id", "SIMPLESAMLPHP_SP_NAME_ID_FORMAT": ( "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" @@ -107,7 +102,7 @@ def get_container_specs(self) -> dict[str, Any] | None: self.url("authentik_sources_saml:acs", source_slug=self.slug) ), }, - } + ) @retry() @apply_blueprint( diff --git a/tests/e2e/test_source_scim.py b/tests/e2e/test_source_scim.py index 26d406fc6048..368bd1811d0e 100644 --- a/tests/e2e/test_source_scim.py +++ b/tests/e2e/test_source_scim.py @@ -2,7 +2,6 @@ from pprint import pformat from time import sleep -from typing import Any from docker.types import Healthcheck @@ -20,22 +19,18 @@ class TestSourceSCIM(SeleniumTestCase): def setUp(self): self.slug = generate_id() super().setUp() - - def get_container_specs(self) -> dict[str, Any] | None: - return { - "image": ( + self.run_container( + image=( "ghcr.io/suvera/scim2-compliance-test-utility@sha256:eca913bb73" "c46892cd1fb2dfd2fef1c5881e6abc5cb0eec7e92fb78c1b933ece" ), - "detach": True, - "ports": {"8080": "8080"}, - "auto_remove": True, - "healthcheck": Healthcheck( + ports={"8080": "8080"}, + healthcheck=Healthcheck( test=["CMD", "curl", "http://localhost:8080"], interval=5 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000, ), - } + ) @retry() def test_scim_conformance(self): diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index ee715f80157b..e118b0d110d5 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -9,6 +9,7 @@ from sys import stderr from time import sleep from typing import Any +from unittest.case import TestCase from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase @@ -19,6 +20,7 @@ from docker import DockerClient, from_env from docker.errors import DockerException from docker.models.containers import Container +from docker.models.networks import Network from selenium import webdriver from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException from selenium.webdriver.common.by import By @@ -31,6 +33,7 @@ from authentik.core.api.users import UserSerializer from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user +from authentik.lib.generators import generate_id RETRIES = int(environ.get("RETRIES", "3")) IS_CI = "CI" in environ @@ -54,9 +57,32 @@ def get_local_ip() -> str: return ip_addr -class DockerTestCase: +class DockerTestCase(TestCase): """Mixin for dealing with containers""" + max_healthcheck_attempts = 30 + + __client: DockerClient + __network: Network + + __label_id = generate_id() + + def setUp(self) -> None: + self.__client = from_env() + self.__network = self.docker_client.networks.create(name=f"authentik-test-{generate_id()}") + + @property + def docker_client(self) -> DockerClient: + return self.__client + + @property + def docker_network(self) -> Network: + return self.__network + + @property + def docker_labels(self) -> dict: + return {"io.goauthentik.test": self.__label_id} + def wait_for_container(self, container: Container): """Check that container is health""" attempt = 0 @@ -67,47 +93,30 @@ def wait_for_container(self, container: Container): return container sleep(1) attempt += 1 - if attempt >= 30: # noqa: PLR2004 + if attempt >= self.max_healthcheck_attempts: self.failureException("Container failed to start") - -class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): - """StaticLiveServerTestCase which automatically creates a Webdriver instance""" - - host = get_local_ip() - container: Container | None = None - wait_timeout: int - user: User - - def setUp(self): - if IS_CI: - print("::group::authentik Logs", file=stderr) - super().setUp() - apps.get_app_config("authentik_tenants").ready() - self.wait_timeout = 60 - self.driver = self._get_driver() - self.driver.implicitly_wait(30) - self.wait = WebDriverWait(self.driver, self.wait_timeout) - self.logger = get_logger() - self.user = create_test_admin_user() - if specs := self.get_container_specs(): - self.container = self._start_container(specs) - def get_container_image(self, base: str) -> str: """Try to pull docker image based on git branch, fallback to main if not found.""" - client: DockerClient = from_env() image = f"{base}:gh-main" try: branch_image = f"{base}:{get_docker_tag()}" - client.images.pull(branch_image) + self.docker_client.images.pull(branch_image) return branch_image except DockerException: - client.images.pull(image) + self.docker_client.images.pull(image) return image - def _start_container(self, specs: dict[str, Any]) -> Container: - client: DockerClient = from_env() - container = client.containers.run(**specs) + def run_container(self, **specs: dict[str, Any]) -> Container: + if "network_mode" not in specs: + specs["network"] = self.__network.name + specs["labels"] = self.docker_labels + specs["detach"] = True + specs["auto_remove"] = True + if hasattr(self, "live_server_url"): + specs.setdefault("environment", {}) + specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url + container = self.docker_client.containers.run(**specs) container.reload() state = container.attrs.get("State", {}) if "Health" not in state: @@ -117,25 +126,53 @@ def _start_container(self, specs: dict[str, Any]) -> Container: def output_container_logs(self, container: Container | None = None): """Output the container logs to our STDOUT""" - _container = container or self.container if IS_CI: - image = _container.image + image = container.image tags = image.tags[0] if len(image.tags) > 0 else str(image) print(f"::group::Container logs - {tags}") - for log in _container.logs().decode().split("\n"): + for log in container.logs().decode().split("\n"): print(log) if IS_CI: print("::endgroup::") - def get_container_specs(self) -> dict[str, Any] | None: - """Optionally get container specs which will launched on setup, wait for the container to - be healthy, and deleted again on tearDown""" - return None + def tearDown(self): + containers = self.docker_client.containers.list( + filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())} + ) + for container in containers: + self.output_container_logs(container) + try: + container.kill() + except DockerException: + pass + self.__network.remove() + + +class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): + """StaticLiveServerTestCase which automatically creates a Webdriver instance""" + + host = get_local_ip() + wait_timeout: int + user: User + + def setUp(self): + if IS_CI: + print("::group::authentik Logs", file=stderr) + apps.get_app_config("authentik_tenants").ready() + self.wait_timeout = 60 + self.driver = self._get_driver() + self.driver.implicitly_wait(30) + self.wait = WebDriverWait(self.driver, self.wait_timeout) + self.logger = get_logger() + self.user = create_test_admin_user() + super().setUp() def _get_driver(self) -> WebDriver: count = 0 try: - return webdriver.Chrome() + opts = webdriver.ChromeOptions() + opts.add_argument("--disable-search-engine-choice-screen") + return webdriver.Chrome(options=opts) except WebDriverException: pass while count < RETRIES: @@ -151,18 +188,15 @@ def _get_driver(self) -> WebDriver: raise ValueError(f"Webdriver failed after {RETRIES}.") def tearDown(self): - super().tearDown() if IS_CI: print("::endgroup::", file=stderr) + super().tearDown() if IS_CI: print("::group::Browser logs") for line in self.driver.get_log("browser"): print(line["message"]) if IS_CI: print("::endgroup::") - if self.container: - self.output_container_logs() - self.container.kill() self.driver.quit() def wait_for_url(self, desired_url): diff --git a/tests/integration/test_outpost_docker.py b/tests/integration/test_outpost_docker.py index 7fd6dfd3fdc7..26d4c0993c7f 100644 --- a/tests/integration/test_outpost_docker.py +++ b/tests/integration/test_outpost_docker.py @@ -6,8 +6,6 @@ import pytest import yaml from channels.testing import ChannelsLiveServerTestCase -from docker import DockerClient, from_env -from docker.models.containers import Container from docker.types.healthcheck import Healthcheck from authentik.core.tests.utils import create_test_flow @@ -27,11 +25,11 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase): """Test Docker Controllers""" - def _start_container(self, ssl_folder: str) -> Container: - client: DockerClient = from_env() - container = client.containers.run( + def setUp(self): + super().setUp() + self.ssl_folder = mkdtemp() + self.run_container( image="library/docker:dind", - detach=True, network_mode="host", privileged=True, healthcheck=Healthcheck( @@ -41,18 +39,11 @@ def _start_container(self, ssl_folder: str) -> Container: ), environment={"DOCKER_TLS_CERTDIR": "/ssl"}, volumes={ - f"{ssl_folder}/": { + f"{self.ssl_folder}/": { "bind": "/ssl", } }, ) - self.wait_for_container(container) - return container - - def setUp(self): - super().setUp() - self.ssl_folder = mkdtemp() - self.container = self._start_container(self.ssl_folder) # Ensure that local connection have been created outpost_connection_discovery() self.provider: ProxyProvider = ProxyProvider.objects.create( @@ -91,7 +82,6 @@ def setUp(self): def tearDown(self) -> None: super().tearDown() - self.container.kill() try: rmtree(self.ssl_folder) except PermissionError: diff --git a/tests/integration/test_proxy_docker.py b/tests/integration/test_proxy_docker.py index 9aefccf39d27..a11116da27d7 100644 --- a/tests/integration/test_proxy_docker.py +++ b/tests/integration/test_proxy_docker.py @@ -6,8 +6,6 @@ import pytest import yaml from channels.testing.live import ChannelsLiveServerTestCase -from docker import DockerClient, from_env -from docker.models.containers import Container from docker.types.healthcheck import Healthcheck from authentik.core.tests.utils import create_test_flow @@ -27,11 +25,11 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase): """Test Docker Controllers""" - def _start_container(self, ssl_folder: str) -> Container: - client: DockerClient = from_env() - container = client.containers.run( + def setUp(self): + super().setUp() + self.ssl_folder = mkdtemp() + self.run_container( image="library/docker:dind", - detach=True, network_mode="host", privileged=True, healthcheck=Healthcheck( @@ -41,18 +39,11 @@ def _start_container(self, ssl_folder: str) -> Container: ), environment={"DOCKER_TLS_CERTDIR": "/ssl"}, volumes={ - f"{ssl_folder}/": { + f"{self.ssl_folder}/": { "bind": "/ssl", } }, ) - self.wait_for_container(container) - return container - - def setUp(self): - super().setUp() - self.ssl_folder = mkdtemp() - self.container = self._start_container(self.ssl_folder) # Ensure that local connection have been created outpost_connection_discovery() self.provider: ProxyProvider = ProxyProvider.objects.create( @@ -91,7 +82,6 @@ def setUp(self): def tearDown(self) -> None: super().tearDown() - self.container.kill() try: rmtree(self.ssl_folder) except PermissionError: diff --git a/website/docs/add-secure-apps/providers/proxy/_traefik_standalone.md b/website/docs/add-secure-apps/providers/proxy/_traefik_standalone.md index b3fff79d1e35..1423c747cbb1 100644 --- a/website/docs/add-secure-apps/providers/proxy/_traefik_standalone.md +++ b/website/docs/add-secure-apps/providers/proxy/_traefik_standalone.md @@ -32,7 +32,7 @@ http: app: loadBalancer: servers: - - url: http://ipp.internal + - url: http://ip.internal authentik: loadBalancer: servers: