Compare commits
10 Commits
permission
...
enterprise
Author | SHA1 | Date | |
---|---|---|---|
60000812fd | |||
929e70669a | |||
36114284bf | |||
a65ea0de94 | |||
2056b0cbee | |||
af85ddf60b | |||
e8e42261e3 | |||
9fda4e91ad | |||
a11f1258e1 | |||
a97578ac62 |
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -168,6 +168,8 @@ jobs:
|
||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||
- name: ldap
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: rac
|
||||
glob: tests/e2e/test_provider_rac*
|
||||
- name: radius
|
||||
glob: tests/e2e/test_provider_radius*
|
||||
- name: scim
|
||||
|
@ -31,6 +31,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
||||
|
||||
if kwargs.get("randomly_seed", None):
|
||||
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
|
||||
if kwargs.get("no_capture"):
|
||||
self.args.append("-s")
|
||||
|
||||
settings.TEST = True
|
||||
settings.CELERY["task_always_eager"] = True
|
||||
@ -56,6 +58,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
||||
def add_arguments(cls, parser: ArgumentParser):
|
||||
"""Add more pytest-specific arguments"""
|
||||
DiscoverRunner.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--no-capture",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--randomly-seed",
|
||||
type=int,
|
||||
|
44
tests/e2e/_process.py
Normal file
44
tests/e2e/_process.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
from datetime import timedelta
|
||||
from time import mktime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from daphne.testing import DaphneProcess
|
||||
from django import setup as django_setup
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestDatabaseProcess(DaphneProcess):
|
||||
"""Channels does not correctly switch to the test database by default.
|
||||
https://github.com/django/channels/issues/2048"""
|
||||
|
||||
def run(self):
|
||||
if not settings.configured: # Fix For raise AppRegistryNotReady("Apps aren't loaded yet.")
|
||||
django_setup() # Ensure Django is fully set up before using settings
|
||||
if not settings.DATABASES[list(settings.DATABASES.keys())[0]]["NAME"].startswith("test_"):
|
||||
for _, db_settings in settings.DATABASES.items():
|
||||
db_settings["NAME"] = f"test_{db_settings['NAME']}"
|
||||
settings.TEST = True
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
),
|
||||
CONFIG.patch("email.port", 1025),
|
||||
):
|
||||
return super().run()
|
@ -1,7 +1,6 @@
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
|
||||
from guardian.shortcuts import assign_perm
|
||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||
@ -56,17 +55,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
outpost.providers.add(ldap)
|
||||
|
||||
self.start_ldap(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)
|
||||
self.wait_for_outpost(outpost)
|
||||
return outpost
|
||||
|
||||
@retry()
|
||||
|
@ -7,7 +7,6 @@ from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skip, skipUnless
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from jwt import decode
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
@ -87,17 +86,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(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)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
self.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
@ -168,17 +157,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(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)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
self.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
@ -202,7 +181,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
# TODO: Fix flaky test
|
||||
@skip("Flaky test")
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
class TestProviderProxyConnect(SeleniumTestCase):
|
||||
"""Test Proxy connectivity over websockets"""
|
||||
|
||||
@retry(exceptions=[AssertionError])
|
||||
@ -239,16 +218,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
# 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 and state.version:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
state = outpost.state
|
||||
self.assertGreaterEqual(len(state), 1)
|
||||
|
@ -76,17 +76,7 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
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)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
@retry()
|
||||
def test_traefik(self):
|
||||
|
126
tests/e2e/test_provider_rac.py
Normal file
126
tests/e2e/test_provider_rac.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""RAC e2e tests"""
|
||||
|
||||
from datetime import timedelta
|
||||
from time import mktime, sleep
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.utils.timezone import now
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||
from authentik.core.models import Application
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
|
||||
class TestProviderRAC(SeleniumTestCase):
|
||||
"""RAC e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.password = generate_id()
|
||||
|
||||
def start_rac(self, outpost: Outpost):
|
||||
"""Start rac container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-rac"),
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
@retry()
|
||||
@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-rac.yaml",
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_rac_ssh(self):
|
||||
"""Test SSH RAC"""
|
||||
License.objects.create(key=generate_id())
|
||||
|
||||
test_ssh = self.run_container(
|
||||
image="lscr.io/linuxserver/openssh-server:latest",
|
||||
ports={
|
||||
"2222": "2222",
|
||||
},
|
||||
environment={
|
||||
"USER_NAME": "authentik",
|
||||
"USER_PASSWORD": self.password,
|
||||
"PASSWORD_ACCESS": "true",
|
||||
"SUDO_ACCESS": "true",
|
||||
},
|
||||
)
|
||||
|
||||
rac: RACProvider = RACProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
)
|
||||
endpoint = Endpoint.objects.create(
|
||||
name=generate_id(),
|
||||
protocol=Protocols.SSH,
|
||||
host=f"{self.host}:2222",
|
||||
settings={
|
||||
"username": "authentik",
|
||||
"password": self.password,
|
||||
},
|
||||
provider=rac,
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=rac)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.RAC,
|
||||
)
|
||||
outpost.providers.add(rac)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_rac(outpost)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
self.driver.get(
|
||||
self.url("authentik_providers_rac:start", app=app.slug, endpoint=endpoint.pk)
|
||||
)
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
iface = self.driver.find_element(By.CSS_SELECTOR, "ak-rac")
|
||||
sleep(5)
|
||||
state = self.driver.execute_script("return arguments[0].clientState", iface)
|
||||
self.assertEqual(state, 3)
|
||||
|
||||
uid = generate_id()
|
||||
self.driver.find_element(By.CSS_SELECTOR, "body").send_keys(
|
||||
f'echo "{uid}" > /tmp/test' + Keys.ENTER
|
||||
)
|
||||
|
||||
sleep(2)
|
||||
|
||||
_, output = test_ssh.exec_run("cat /tmp/test")
|
||||
self.assertEqual(output, f"{uid}\n".encode())
|
@ -1,7 +1,6 @@
|
||||
"""Radius e2e tests"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
|
||||
from pyrad.client import Client
|
||||
from pyrad.dictionary import Dictionary
|
||||
@ -50,17 +49,7 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
outpost.providers.add(radius)
|
||||
|
||||
self.start_radius(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)
|
||||
self.wait_for_outpost(outpost)
|
||||
return outpost
|
||||
|
||||
@retry()
|
||||
|
@ -419,7 +419,6 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
# sleep(999999)
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
.exclude(username__startswith="ak-outpost")
|
||||
|
@ -12,8 +12,8 @@ from typing import Any
|
||||
from unittest.case import TestCase
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from django.apps import apps
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from django.db import connection
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.test.testcases import TransactionTestCase
|
||||
@ -35,6 +35,8 @@ 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
|
||||
from authentik.outposts.models import Outpost
|
||||
from tests.e2e._process import TestDatabaseProcess
|
||||
|
||||
RETRIES = int(environ.get("RETRIES", "3"))
|
||||
IS_CI = "CI" in environ
|
||||
@ -58,10 +60,13 @@ def get_local_ip() -> str:
|
||||
return ip_addr
|
||||
|
||||
|
||||
class ContainerException(Exception): ...
|
||||
|
||||
|
||||
class DockerTestCase(TestCase):
|
||||
"""Mixin for dealing with containers"""
|
||||
|
||||
max_healthcheck_attempts = 30
|
||||
max_healthcheck_attempts = 50
|
||||
|
||||
__client: DockerClient
|
||||
__network: Network
|
||||
@ -95,7 +100,7 @@ class DockerTestCase(TestCase):
|
||||
sleep(1)
|
||||
attempt += 1
|
||||
if attempt >= self.max_healthcheck_attempts:
|
||||
self.failureException("Container failed to start")
|
||||
raise ContainerException("Container failed to start")
|
||||
|
||||
def get_container_image(self, base: str) -> str:
|
||||
"""Try to pull docker image based on git branch, fallback to main if not found."""
|
||||
@ -152,13 +157,17 @@ class DockerTestCase(TestCase):
|
||||
self.__network.remove()
|
||||
|
||||
|
||||
class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
class SeleniumTestCase(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
"""ChannelsLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
|
||||
ProtocolServerProcess = TestDatabaseProcess
|
||||
|
||||
host = get_local_ip()
|
||||
wait_timeout: int
|
||||
user: User
|
||||
|
||||
serve_static = True
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
@ -265,6 +274,18 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
self.assertEqual(user["name"].value, expected_user.name)
|
||||
self.assertEqual(user["email"].value, expected_user.email)
|
||||
|
||||
def wait_for_outpost(self, outpost: Outpost, tries=50):
|
||||
"""Wait until outpost healthcheck succeeds"""
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < tries:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
return
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
raise ContainerException("Outpost failed to become healthy")
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_loader():
|
||||
@ -277,7 +298,12 @@ def retry(max_retires=RETRIES, exceptions=None):
|
||||
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||
|
||||
if not exceptions:
|
||||
exceptions = [WebDriverException, TimeoutException, NoSuchElementException]
|
||||
exceptions = [
|
||||
WebDriverException,
|
||||
TimeoutException,
|
||||
NoSuchElementException,
|
||||
ContainerException,
|
||||
]
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
Reference in New Issue
Block a user