tests/e2e: add forward auth e2e test (#11374)
* add nginx forward_auth e2e tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add envoy Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove even more duplicate code Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add traefik static config Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more cleanup, don't generate dex config cause they support env variables Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use default dex entrypoint to use templating Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove options that are always set as default Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix compose flag Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add caddy Signed-off-by: Jens Langhammer <jens@goauthentik.io> * merge python files Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use whoami api to check better Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix envoy config Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set invalidation flow Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix logout checks Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -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:
|
||||
|
21
tests/e2e/proxy_forward_auth/caddy_single/Caddyfile
Normal file
21
tests/e2e/proxy_forward_auth/caddy_single/Caddyfile
Normal file
@ -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
|
||||
}
|
||||
}
|
99
tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml
Normal file
99
tests/e2e/proxy_forward_auth/envoy_single/envoy.yaml
Normal file
@ -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
|
59
tests/e2e/proxy_forward_auth/nginx_single/nginx.conf
Normal file
59
tests/e2e/proxy_forward_auth/nginx_single/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
23
tests/e2e/sources_oauth2_dex/dex.yaml
Normal file
23
tests/e2e/sources_oauth2_dex/dex.yaml
Normal file
@ -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
|
@ -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 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
||||
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 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
)
|
||||
outpost.providers.add(ldap)
|
||||
|
||||
self.ldap_container = self.start_ldap(outpost)
|
||||
self.start_ldap(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
|
@ -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 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
"GF_AUTH_GITHUB_API_URL": self.url("authentik_providers_oauth2_root:github-user"),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
|
@ -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 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
|
@ -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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
"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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
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")))
|
||||
|
@ -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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
"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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
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 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
)
|
||||
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()
|
||||
|
@ -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 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
||||
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 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
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 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
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 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
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 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
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)
|
||||
|
227
tests/e2e/test_provider_proxy_forward.py
Normal file
227
tests/e2e/test_provider_proxy_forward.py
Normal file
@ -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)
|
@ -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 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
||||
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 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
)
|
||||
outpost.providers.add(radius)
|
||||
|
||||
self.radius_container = self.start_radius(outpost)
|
||||
self.start_radius(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
|
@ -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 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
)
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
"SP_METADATA_URL": metadata_url,
|
||||
},
|
||||
)
|
||||
self.wait_for_container(container)
|
||||
return container
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@ -85,7 +77,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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/")
|
||||
|
@ -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 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
||||
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 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
||||
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)
|
||||
|
@ -56,14 +56,10 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
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 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
source_slug=self.source_slug,
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
|
@ -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.sources.oauth.models import OAuthSource
|
||||
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"""
|
||||
|
@ -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 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
"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 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.url("authentik_sources_saml:acs", source_slug=self.slug)
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
|
@ -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):
|
||||
|
@ -9,6 +9,7 @@ from os import environ
|
||||
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 django.urls import reverse
|
||||
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 structlog.stdlib import get_logger
|
||||
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 @@ class DockerTestCase:
|
||||
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 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
|
||||
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 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
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):
|
||||
|
@ -6,8 +6,6 @@ from tempfile import mkdtemp
|
||||
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 @@ from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
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 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
),
|
||||
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 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
self.container.kill()
|
||||
try:
|
||||
rmtree(self.ssl_folder)
|
||||
except PermissionError:
|
||||
|
@ -6,8 +6,6 @@ from tempfile import mkdtemp
|
||||
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 @@ from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
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 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
),
|
||||
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 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
self.container.kill()
|
||||
try:
|
||||
rmtree(self.ssl_folder)
|
||||
except PermissionError:
|
||||
|
@ -32,7 +32,7 @@ http:
|
||||
app:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://ipp.internal
|
||||
- url: http://ip.internal
|
||||
authentik:
|
||||
loadBalancer:
|
||||
servers:
|
||||
|
Reference in New Issue
Block a user