blueprints: migrate from managed (#3338)

* test all bundled blueprints

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix empty title

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix default blueprints

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add script to generate dev config

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* migrate managed to blueprints

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add more to blueprint instance

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* migrated away from ObjectManager

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix lint errors

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* migrate things

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* migrate tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix some tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix a bit more

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix more tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* whops

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix missing name

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* *sigh*

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix more tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add tasks

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* scheduled

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* run discovery on start

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* oops this test should stay

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-08-01 23:05:58 +02:00 committed by GitHub
parent 7a05c6faef
commit a023eee9bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 1094 additions and 871 deletions

View file

@ -73,7 +73,7 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir -p /certs /media && \
mkdir -p /certs /media /blueprints && \
mkdir -p /authentik/.ssh && \
chown authentik:authentik /certs /media /authentik/.ssh
@ -82,7 +82,8 @@ COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints/default /blueprints
COPY ./blueprints/default /blueprints/default
COPY ./blueprints/system /blueprints/system
COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy
COPY --from=web-builder /work/web/dist/ /web/dist/

View file

@ -33,8 +33,8 @@ test:
coverage report
lint-fix:
isort authentik tests lifecycle
black authentik tests lifecycle
isort authentik tests scripts lifecycle
black authentik tests scripts lifecycle
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
authentik \
internal \
@ -91,6 +91,9 @@ gen-client-go:
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
rm -rf config.yaml ./templates/
gen-dev-config:
python -m scripts.generate_config
gen: gen-build gen-clean gen-client-web
migrate:

View file

@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost

View file

@ -1,19 +1,20 @@
"""authentik admin app config"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge, Info
from authentik.blueprints.manager import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class AuthentikAdminConfig(AppConfig):
class AuthentikAdminConfig(ManagedAppConfig):
"""authentik admin app config"""
name = "authentik.admin"
label = "authentik_admin"
verbose_name = "authentik Admin"
default = True
def ready(self):
import_module("authentik.admin.signals")
def reconcile_load_admin_signals(self):
"""Load admin signals"""
self.import_module("authentik.admin.signals")

View file

@ -1,11 +1,11 @@
"""test admin api"""
from json import loads
from django.apps import apps
from django.test import TestCase
from django.urls import reverse
from authentik import __version__
from authentik.blueprints.tasks import managed_reconcile
from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models
from authentik.events.monitored_tasks import TaskResultStatus
@ -95,7 +95,6 @@ class TestAdminAPI(TestCase):
def test_system(self):
"""Test system API"""
# pyright: reportGeneralTypeIssues=false
managed_reconcile() # pylint: disable=no-value-for-parameter
apps.get_app_config("authentik_outposts").reconcile_embedded_outpost()
response = self.client.get(reverse("authentik_api:admin_system"))
self.assertEqual(response.status_code, 200)

View file

@ -65,7 +65,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
def token_secret_key(value: str) -> Optional[User]:
"""Check if the token is the secret key
and return the service account for the managed outpost"""
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
if value != settings.SECRET_KEY:
return None

View file

@ -1,6 +1,7 @@
"""Test API Authentication"""
from base64 import b64encode
from django.apps import apps
from django.conf import settings
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
@ -10,7 +11,6 @@ from authentik.api.authentication import bearer_auth
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
from authentik.core.tests.utils import create_test_flow
from authentik.lib.generators import generate_id
from authentik.outposts.managed import OutpostManager
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken
@ -44,7 +44,7 @@ class TestAPIAuth(TestCase):
with self.assertRaises(AuthenticationFailed):
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
OutpostManager().run()
apps.get_app_config("authentik_outposts").reconcile_embedded_outpost()
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)

View file

@ -0,0 +1,23 @@
"""Blueprint helpers"""
from functools import wraps
from typing import Callable
def apply_blueprint(*files: str):
"""Apply blueprint before test"""
from authentik.blueprints.v1.importer import Importer
def wrapper_outer(func: Callable):
"""Apply blueprint before test"""
@wraps(func)
def wrapper(*args, **kwargs):
for file in files:
with open(file, "r+", encoding="utf-8") as _file:
Importer(_file.read()).apply()
return func(*args, **kwargs)
return wrapper
return wrapper_outer

View file

@ -27,13 +27,21 @@ class BlueprintInstanceSerializer(ModelSerializer):
model = BlueprintInstance
fields = [
"pk",
"name",
"path",
"context",
"last_applied",
"last_applied_hash",
"status",
"enabled",
"managed_models",
]
extra_kwargs = {
"last_applied": {"read_only": True},
"last_applied_hash": {"read_only": True},
"managed_models": {"read_only": True},
}
class BlueprintInstanceViewSet(ModelViewSet):

View file

@ -1,15 +1,22 @@
"""authentik Blueprints app"""
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikBlueprintsConfig(AppConfig):
class AuthentikBlueprintsConfig(ManagedAppConfig):
"""authentik Blueprints app"""
name = "authentik.blueprints"
label = "authentik_blueprints"
verbose_name = "authentik Blueprints"
default = True
def ready(self) -> None:
from authentik.blueprints.tasks import managed_reconcile
def reconcile_load_blueprints_v1_tasks(self):
"""Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks")
managed_reconcile.delay()
def reconcile_blueprints_discover(self):
"""Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discover
blueprints_discover.delay()

View file

@ -13,9 +13,9 @@ class Command(BaseCommand): # pragma: no cover
for blueprint_path in options.get("blueprints", []):
with open(blueprint_path, "r", encoding="utf8") as blueprint_file:
importer = Importer(blueprint_file.read())
valid = importer.validate()
valid, logs = importer.validate()
if not valid:
raise ValueError("blueprint invalid")
raise ValueError(f"blueprint invalid: {logs}")
importer.apply()
def add_arguments(self, parser):

View file

@ -1,70 +1,37 @@
"""Managed objects manager"""
from typing import Callable, Optional
from importlib import import_module
from inspect import ismethod
from django.apps import AppConfig
from django.db import DatabaseError, ProgrammingError
from structlog.stdlib import get_logger
from authentik.blueprints.models import ManagedModel
LOGGER = get_logger()
class EnsureOp:
"""Ensure operation, executed as part of an ObjectManager run"""
class ManagedAppConfig(AppConfig):
"""Basic reconciliation logic for apps"""
_obj: type[ManagedModel]
_managed_uid: str
_kwargs: dict
def ready(self) -> None:
self.reconcile()
return super().ready()
def __init__(self, obj: type[ManagedModel], managed_uid: str, **kwargs) -> None:
self._obj = obj
self._managed_uid = managed_uid
self._kwargs = kwargs
def import_module(self, path: str):
"""Load module"""
import_module(path)
def run(self):
"""Do the actual ensure action"""
raise NotImplementedError
class EnsureExists(EnsureOp):
"""Ensure object exists, with kwargs as given values"""
created_callback: Optional[Callable]
def __init__(
self,
obj: type[ManagedModel],
managed_uid: str,
created_callback: Optional[Callable] = None,
**kwargs,
) -> None:
super().__init__(obj, managed_uid, **kwargs)
self.created_callback = created_callback
def run(self):
self._kwargs.setdefault("managed", self._managed_uid)
obj, created = self._obj.objects.update_or_create(
**{
"managed": self._managed_uid,
"defaults": self._kwargs,
}
)
if created and self.created_callback is not None:
self.created_callback(obj)
class ObjectManager:
"""Base class for Apps Object manager"""
def run(self):
"""Main entrypoint for tasks, iterate through all implementation of this
and execute all operations"""
for sub in ObjectManager.__subclasses__():
sub_inst = sub()
ops = sub_inst.reconcile()
LOGGER.debug("Reconciling managed objects", manager=sub.__name__)
for operation in ops:
operation.run()
def reconcile(self) -> list[EnsureOp]:
"""Method which is implemented in subclass that returns a list of Operations"""
raise NotImplementedError
def reconcile(self) -> None:
"""reconcile ourselves"""
prefix = "reconcile_"
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
continue
if not meth_name.startswith(prefix):
continue
name = meth_name.replace(prefix, "")
try:
meth()
LOGGER.debug("Successfully reconciled", name=name)
except (ProgrammingError, DatabaseError) as exc:
LOGGER.debug("Failed to run reconcile", name=name, exc=exc)

View file

@ -1,16 +1,37 @@
# Generated by Django 4.0.6 on 2022-07-30 22:45
# Generated by Django 4.0.6 on 2022-07-31 17:35
import uuid
import django.contrib.postgres.fields
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.blueprints.v1.tasks import blueprints_discover
BlueprintInstance = apps.get_model("authentik_blueprints", "BlueprintInstance")
Flow = apps.get_model("authentik_flows", "Flow")
db_alias = schema_editor.connection.alias
blueprints_discover()
for blueprint in BlueprintInstance.objects.using(db_alias).all():
# If we already have flows (and we should always run before flow migrations)
# then this is an existing install and we want to disable all blueprints
if Flow.objects.using(db_alias).all().exists():
blueprint.enabled = False
# System blueprints are always enabled
if "/system/" in blueprint.path:
blueprint.enabled = True
blueprint.save()
class Migration(migrations.Migration):
initial = True
dependencies = []
dependencies = [("authentik_flows", "0001_initial")]
operations = [
migrations.CreateModel(
@ -38,6 +59,7 @@ class Migration(migrations.Migration):
("path", models.TextField()),
("context", models.JSONField()),
("last_applied", models.DateTimeField(auto_now=True)),
("last_applied_hash", models.TextField()),
(
"status",
models.TextField(
@ -45,6 +67,7 @@ class Migration(migrations.Migration):
("successful", "Successful"),
("warning", "Warning"),
("error", "Error"),
("orphaned", "Orphaned"),
("unknown", "Unknown"),
]
),
@ -63,4 +86,5 @@ class Migration(migrations.Migration):
"unique_together": {("name", "path")},
},
),
migrations.RunPython(migration_blueprint_import),
]

View file

@ -38,6 +38,7 @@ class BlueprintInstanceStatus(models.TextChoices):
SUCCESSFUL = "successful"
WARNING = "warning"
ERROR = "error"
ORPHANED = "orphaned"
UNKNOWN = "unknown"
@ -51,6 +52,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
path = models.TextField()
context = models.JSONField()
last_applied = models.DateTimeField(auto_now=True)
last_applied_hash = models.TextField()
status = models.TextField(choices=BlueprintInstanceStatus.choices)
enabled = models.BooleanField(default=True)
managed_models = ArrayField(models.TextField())

View file

@ -1,17 +1,12 @@
"""managed Settings"""
"""blueprint Settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"blueprints_reconcile": {
"task": "authentik.blueprints.tasks.managed_reconcile",
"schedule": crontab(minute=fqdn_rand("managed_reconcile"), hour="*/4"),
"options": {"queue": "authentik_scheduled"},
},
"blueprints_config_file_discovery": {
"task": "authentik.blueprints.tasks.config_file_discovery",
"schedule": crontab(minute=fqdn_rand("config_file_discovery"), hour="*"),
"blueprints_v1_discover": {
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},
}

View file

@ -1,29 +0,0 @@
"""managed tasks"""
from django.db import DatabaseError
from django.db.utils import ProgrammingError
from authentik.blueprints.manager import ObjectManager
from authentik.core.tasks import CELERY_APP
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
@CELERY_APP.task(
bind=True,
base=MonitoredTask,
retry_backoff=True,
)
@prefill_task
def managed_reconcile(self: MonitoredTask):
"""Run ObjectManager to ensure objects are up-to-date"""
try:
ObjectManager().run()
self.set_status(
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."])
)
except (DatabaseError, ProgrammingError) as exc: # pragma: no cover
self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)]))

View file

@ -0,0 +1,30 @@
"""test packaged blueprints"""
from glob import glob
from pathlib import Path
from typing import Callable
from django.test import TransactionTestCase
from django.utils.text import slugify
from authentik.blueprints.v1.importer import Importer
class TestBundled(TransactionTestCase):
"""Empty class, test methods are added dynamically"""
def blueprint_tester(file_name: str) -> Callable:
"""This is used instead of subTest for better visibility"""
def tester(self: TestBundled):
with open(file_name, "r", encoding="utf8") as flow_yaml:
importer = Importer(flow_yaml.read())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
return tester
for flow_file in glob("blueprints/**/*.yaml", recursive=True):
method_name = slugify(Path(flow_file).stem).replace("-", "_").replace(".", "_")
setattr(TestBundled, f"test_flow_{method_name}", blueprint_tester(flow_file))

View file

@ -1,13 +0,0 @@
"""managed tests"""
from django.test import TestCase
from authentik.blueprints.tasks import managed_reconcile
class TestManaged(TestCase):
"""managed tests"""
def test_reconcile(self):
"""Test reconcile"""
# pyright: reportGeneralTypeIssues=false
managed_reconcile() # pylint: disable=no-value-for-parameter

View file

@ -37,14 +37,14 @@ class TestFlowTransport(TransactionTestCase):
def test_bundle_invalid_format(self):
"""Test bundle with invalid format"""
importer = Importer('{"version": 3}')
self.assertFalse(importer.validate())
self.assertFalse(importer.validate()[0])
importer = Importer(
(
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
'"model": "authentik_core.User"}]}'
)
)
self.assertFalse(importer.validate())
self.assertFalse(importer.validate()[0])
def test_export_validate_import(self):
"""Test export and validate it"""
@ -70,7 +70,7 @@ class TestFlowTransport(TransactionTestCase):
export_yaml = exporter.export_to_string()
importer = Importer(export_yaml)
self.assertTrue(importer.validate())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
@ -80,7 +80,7 @@ class TestFlowTransport(TransactionTestCase):
count_initial = Prompt.objects.filter(field_key="username").count()
importer = Importer(STATIC_PROMPT_EXPORT)
self.assertTrue(importer.validate())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count()
@ -116,7 +116,7 @@ class TestFlowTransport(TransactionTestCase):
export_yaml = exporter.export_to_string()
importer = Importer(export_yaml)
self.assertTrue(importer.validate())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
@ -160,5 +160,5 @@ class TestFlowTransport(TransactionTestCase):
importer = Importer(export_yaml)
self.assertTrue(importer.validate())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())

View file

@ -1,29 +0,0 @@
"""test example flows in docs"""
from glob import glob
from pathlib import Path
from typing import Callable
from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer
class TestTransportDocs(TransactionTestCase):
"""Empty class, test methods are added dynamically"""
def pbflow_tester(file_name: str) -> Callable:
"""This is used instead of subTest for better visibility"""
def tester(self: TestTransportDocs):
with open(file_name, "r", encoding="utf8") as flow_json:
importer = Importer(flow_json.read())
self.assertTrue(importer.validate())
self.assertTrue(importer.apply())
return tester
for flow_file in glob("website/static/flows/*.yaml"):
method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_")
setattr(TestTransportDocs, f"test_flow_{method_name}", pbflow_tester(flow_file))

View file

@ -13,6 +13,8 @@ from django.db.utils import IntegrityError
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger
from structlog.testing import capture_logs
from structlog.types import EventDict
from yaml import load
from authentik.blueprints.v1.common import (
@ -198,17 +200,20 @@ class Importer:
self.logger.debug("updated model", model=model, pk=model.pk)
return True
def validate(self) -> bool:
"""Validate loaded flow export, ensure all models are allowed
def validate(self) -> tuple[bool, list[EventDict]]:
"""Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors"""
self.logger.debug("Starting flow import validation")
self.logger.debug("Starting blueprint import validation")
orig_import = deepcopy(self.__import)
if self.__import.version != 1:
self.logger.warning("Invalid bundle version")
return False
with transaction_rollback():
return False, []
with (
transaction_rollback(),
capture_logs() as logs,
):
successful = self._apply_models()
if not successful:
self.logger.debug("Flow validation failed")
self.logger.debug("blueprint validation failed")
self.__import = orig_import
return successful
return successful, logs

View file

@ -0,0 +1,84 @@
"""v1 blueprints tasks"""
from glob import glob
from hashlib import sha512
from pathlib import Path
from django.db import DatabaseError, InternalError, ProgrammingError
from yaml import load
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
from authentik.blueprints.v1.common import BlueprintLoader
from authentik.blueprints.v1.importer import Importer
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
@CELERY_APP.task()
@prefill_task
def blueprints_discover():
"""Find blueprints and check if they need to be created in the database"""
for folder in CONFIG.y("blueprint_locations"):
for file in glob(f"{folder}/**/*.yaml", recursive=True):
check_blueprint_v1_file(Path(file))
def check_blueprint_v1_file(path: Path):
"""Check if blueprint should be imported"""
with open(path, "r", encoding="utf-8") as blueprint_file:
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
version = raw_blueprint.get("version", 1)
if version != 1:
return
blueprint_file.seek(0)
file_hash = sha512(path.read_bytes()).hexdigest()
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
if not instance:
instance = BlueprintInstance(
name=path.name,
path=str(path),
context={},
status=BlueprintInstanceStatus.UNKNOWN,
enabled=True,
managed_models=[],
)
instance.save()
if instance.last_applied_hash != file_hash:
apply_blueprint.delay(instance.pk.hex)
instance.last_applied_hash = file_hash
instance.save()
@CELERY_APP.task(
bind=True,
base=MonitoredTask,
)
def apply_blueprint(self: MonitoredTask, instance_pk: str):
"""Apply single blueprint"""
self.save_on_success = False
try:
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
if not instance:
return
with open(instance.path, "r", encoding="utf-8") as blueprint_file:
importer = Importer(blueprint_file.read())
valid, logs = importer.validate()
if not valid:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs]))
return
applied = importer.apply()
if not applied:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply"))
except (DatabaseError, ProgrammingError, InternalError) as exc:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))

View file

@ -1,22 +1,37 @@
"""authentik core app config"""
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikCoreConfig(AppConfig):
class AuthentikCoreConfig(ManagedAppConfig):
"""authentik core app config"""
name = "authentik.core"
label = "authentik_core"
verbose_name = "authentik Core"
mountpoint = ""
default = True
def ready(self):
import_module("authentik.core.signals")
import_module("authentik.core.managed")
def reconcile_load_core_signals(self):
"""Load core signals"""
self.import_module("authentik.core.signals")
def reconcile_debug_worker_hook(self):
"""Dispatch startup tasks inline when debugging"""
if settings.DEBUG:
from authentik.root.celery import worker_ready_hook
worker_ready_hook()
def reconcile_source_inbuilt(self):
"""Reconcile inbuilt source"""
from authentik.core.models import Source
Source.objects.update_or_create(
defaults={
"name": "authentik Built-in",
"slug": "authentik-built-in",
},
managed="goauthentik.io/sources/inbuilt",
)

View file

@ -1,17 +0,0 @@
"""Core managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.core.models import Source
class CoreManager(ObjectManager):
"""Core managed objects"""
def reconcile(self):
return [
EnsureExists(
Source,
"goauthentik.io/sources/inbuilt",
name="authentik Built-in",
slug="authentik-built-in",
),
]

View file

@ -22,8 +22,8 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction

View file

@ -1,16 +1,55 @@
"""authentik crypto app config"""
from importlib import import_module
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
if TYPE_CHECKING:
from authentik.crypto.models import CertificateKeyPair
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
class AuthentikCryptoConfig(AppConfig):
class AuthentikCryptoConfig(ManagedAppConfig):
"""authentik crypto app config"""
name = "authentik.crypto"
label = "authentik_crypto"
verbose_name = "authentik Crypto"
default = True
def ready(self):
import_module("authentik.crypto.managed")
import_module("authentik.crypto.tasks")
def reconcile_load_crypto_tasks(self):
"""Load crypto tasks"""
self.import_module("authentik.crypto.tasks")
def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None):
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
builder = CertificateBuilder()
builder.common_name = "goauthentik.io"
builder.build(
subject_alt_names=["goauthentik.io"],
validity_days=360,
)
if not cert:
cert = CertificateKeyPair()
cert.certificate_data = builder.certificate
cert.key_data = builder.private_key
cert.name = "authentik Internal JWT Certificate"
cert.managed = MANAGED_KEY
cert.save()
def reconcile_managed_jwt_cert(self):
"""Ensure managed JWT certificate"""
from authentik.crypto.models import CertificateKeyPair
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
if not certs.exists():
self._create_update_cert()
return
cert: CertificateKeyPair = certs.first()
now = datetime.now()
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
self._create_update_cert(cert)

View file

@ -1,40 +0,0 @@
"""Crypto managed objects"""
from datetime import datetime
from typing import Optional
from authentik.blueprints.manager import ObjectManager
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
class CryptoManager(ObjectManager):
"""Crypto managed objects"""
def _create(self, cert: Optional[CertificateKeyPair] = None):
builder = CertificateBuilder()
builder.common_name = "goauthentik.io"
builder.build(
subject_alt_names=["goauthentik.io"],
validity_days=360,
)
if not cert:
cert = CertificateKeyPair()
cert.certificate_data = builder.certificate
cert.key_data = builder.private_key
cert.name = "authentik Internal JWT Certificate"
cert.managed = MANAGED_KEY
cert.save()
def reconcile(self):
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
if not certs.exists():
self._create()
return []
cert: CertificateKeyPair = certs.first()
now = datetime.now()
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
self._create(cert)
return []
return []

View file

@ -1,9 +1,8 @@
"""authentik events app"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge
from authentik.blueprints.manager import ManagedAppConfig
GAUGE_TASKS = Gauge(
"authentik_system_tasks",
"System tasks and their status",
@ -11,12 +10,14 @@ GAUGE_TASKS = Gauge(
)
class AuthentikEventsConfig(AppConfig):
class AuthentikEventsConfig(ManagedAppConfig):
"""authentik events app"""
name = "authentik.events"
label = "authentik_events"
verbose_name = "authentik Events"
default = True
def ready(self):
import_module("authentik.events.signals")
def reconcile_load_events_signals(self):
"""Load events signals"""
self.import_module("authentik.events.signals")

View file

@ -168,7 +168,8 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
if not file:
return HttpResponseBadRequest()
importer = Importer(file.read().decode())
valid = importer.validate()
valid, _logs = importer.validate()
# TODO: return logs
if not valid:
return HttpResponseBadRequest()
successful = importer.apply()

View file

@ -1,10 +1,7 @@
"""authentik flows app config"""
from importlib import import_module
from django.apps import AppConfig
from django.db.utils import ProgrammingError
from prometheus_client import Gauge, Histogram
from authentik.blueprints.manager import ManagedAppConfig
from authentik.lib.utils.reflection import all_subclasses
GAUGE_FLOWS_CACHED = Gauge(
@ -18,20 +15,22 @@ HIST_FLOWS_PLAN_TIME = Histogram(
)
class AuthentikFlowsConfig(AppConfig):
class AuthentikFlowsConfig(ManagedAppConfig):
"""authentik flows app config"""
name = "authentik.flows"
label = "authentik_flows"
mountpoint = "flows/"
verbose_name = "authentik Flows"
default = True
def ready(self):
import_module("authentik.flows.signals")
try:
def reconcile_load_flows_signals(self):
"""Load flows signals"""
self.import_module("authentik.flows.signals")
def reconcile_stages_loaded(self):
"""Ensure all stages are loaded"""
from authentik.flows.models import Stage
for stage in all_subclasses(Stage):
_ = stage().type
except ProgrammingError:
pass

View file

@ -62,7 +62,6 @@ ldap:
tls:
ciphers: null
config_file_dir: "/config"
cookie_domain: null
disable_update_check: false
disable_startup_analytics: false
@ -79,3 +78,6 @@ gdpr_compliance: true
cert_discovery_dir: /certs
default_token_length: 128
impersonation: true
blueprint_locations:
- /blueprints

View file

@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType, default_outpost_config
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider

View file

@ -1,10 +1,9 @@
"""authentik outposts app config"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge
from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig
LOGGER = get_logger()
GAUGE_OUTPOSTS_CONNECTED = Gauge(
@ -15,15 +14,47 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
"Last update from any outpost",
["outpost", "uid", "version"],
)
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
class AuthentikOutpostConfig(AppConfig):
class AuthentikOutpostConfig(ManagedAppConfig):
"""authentik outposts app config"""
name = "authentik.outposts"
label = "authentik_outposts"
verbose_name = "authentik Outpost"
default = True
def ready(self):
import_module("authentik.outposts.signals")
import_module("authentik.outposts.managed")
def reconcile_load_outposts_signals(self):
"""Load outposts signals"""
self.import_module("authentik.outposts.signals")
def reconcile_embedded_outpost(self):
"""Ensure embedded outpost"""
from authentik.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
Outpost,
OutpostConfig,
OutpostType,
)
outpost, updated = Outpost.objects.update_or_create(
defaults={
"name": "authentik Embedded Outpost",
"type": OutpostType.PROXY,
},
managed=MANAGED_OUTPOST,
)
if updated:
if KubernetesServiceConnection.objects.exists():
outpost.service_connection = KubernetesServiceConnection.objects.first()
elif DockerServiceConnection.objects.exists():
outpost.service_connection = DockerServiceConnection.objects.first()
outpost.config = OutpostConfig(
kubernetes_disabled_components=[
"deployment",
"secret",
]
)
outpost.save()

View file

@ -14,10 +14,10 @@ from structlog.stdlib import get_logger
from yaml import safe_dump
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
from authentik.outposts.docker_tls import DockerInlineTLS
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.models import (
DockerServiceConnection,
Outpost,

View file

@ -10,8 +10,8 @@ from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
from authentik.outposts.managed import MANAGED_OUTPOST
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController

View file

@ -78,7 +78,7 @@ class DockerInlineSSH:
"""Cleanup when we're done"""
try:
os.unlink(self.key_path)
with open(self.config_path, "r+", encoding="utf-8") as ssh_config:
with open(self.config_path, "r", encoding="utf-8") as ssh_config:
start = 0
end = 0
lines = ssh_config.readlines()

View file

@ -1,41 +0,0 @@
"""Outpost managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
Outpost,
OutpostConfig,
OutpostType,
)
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
class OutpostManager(ObjectManager):
"""Outpost managed objects"""
def reconcile(self):
def outpost_created(outpost: Outpost):
"""When outpost is initially created, and we already have a service connection,
auto-assign it."""
if KubernetesServiceConnection.objects.exists():
outpost.service_connection = KubernetesServiceConnection.objects.first()
elif DockerServiceConnection.objects.exists():
outpost.service_connection = DockerServiceConnection.objects.first()
outpost.config = OutpostConfig(
kubernetes_disabled_components=[
"deployment",
"secret",
]
)
outpost.save()
return [
EnsureExists(
Outpost,
MANAGED_OUTPOST,
created_callback=outpost_created,
name="authentik Embedded Outpost",
type=OutpostType.PROXY,
),
]

View file

@ -233,7 +233,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
def outpost_local_connection():
"""Checks the local environment and create Service connections."""
if not CONFIG.y_bool("outposts.discover"):
LOGGER.debug("outpost integration discovery is disabled")
LOGGER.debug("Outpost integration discovery is disabled")
return
# Explicitly check against token filename, as that's
# only present when the integration is enabled

View file

@ -1,11 +1,11 @@
"""Docker controller tests"""
from django.apps import apps
from django.test import TestCase
from docker.models.containers import Container
from authentik.blueprints.manager import ObjectManager
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.controllers.docker import DockerController
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
from authentik.providers.proxy.controllers.docker import ProxyDockerController
@ -19,7 +19,7 @@ class DockerControllerTests(TestCase):
type=OutpostType.PROXY,
)
self.integration = DockerServiceConnection(name="test")
ObjectManager().run()
apps.get_app_config("authentik_outposts").reconcile()
def test_init_managed(self):
"""Docker controller shouldn't do anything for managed outpost"""

View file

@ -1,9 +1,8 @@
"""authentik policies app config"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge, Histogram
from authentik.blueprints.manager import ManagedAppConfig
GAUGE_POLICIES_CACHED = Gauge(
"authentik_policies_cached",
"Cached Policies",
@ -27,12 +26,14 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
)
class AuthentikPoliciesConfig(AppConfig):
class AuthentikPoliciesConfig(ManagedAppConfig):
"""authentik policies app config"""
name = "authentik.policies"
label = "authentik_policies"
verbose_name = "authentik Policies"
default = True
def ready(self):
import_module("authentik.policies.signals")
def reconcile_load_policies_signals(self):
"""Load policies signals"""
self.import_module("authentik.policies.signals")

View file

@ -1,16 +1,19 @@
"""Authentik reputation_policy app config"""
from importlib import import_module
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikPolicyReputationConfig(AppConfig):
class AuthentikPolicyReputationConfig(ManagedAppConfig):
"""Authentik reputation app config"""
name = "authentik.policies.reputation"
label = "authentik_policies_reputation"
verbose_name = "authentik Policies.Reputation"
default = True
def ready(self):
import_module("authentik.policies.reputation.signals")
import_module("authentik.policies.reputation.tasks")
def reconcile_load_policies_reputation_signals(self):
"""Load policies.reputation signals"""
self.import_module("authentik.policies.reputation.signals")
def reconcile_load_policies_reputation_tasks(self):
"""Load policies.reputation tasks"""
self.import_module("authentik.policies.reputation.tasks")

View file

@ -1,6 +1,4 @@
"""authentik oauth provider app config"""
from importlib import import_module
from django.apps import AppConfig
@ -14,6 +12,3 @@ class AuthentikProviderOAuth2Config(AppConfig):
"authentik.providers.oauth2.urls_github": "",
"authentik.providers.oauth2.urls": "application/o/",
}
def ready(self) -> None:
import_module("authentik.providers.oauth2.managed")

View file

@ -1,60 +0,0 @@
"""OAuth2 Provider managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.providers.oauth2.models import ScopeMapping
SCOPE_OPENID_EXPRESSION = """
# This scope is required by the OpenID-spec, and must as such exist in authentik.
# The scope by itself does not grant any information
return {}
"""
SCOPE_EMAIL_EXPRESSION = """
return {
"email": request.user.email,
"email_verified": True
}
"""
SCOPE_PROFILE_EXPRESSION = """
return {
# Because authentik only saves the user's full name, and has no concept of first and last names,
# the full name is used as given name.
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
"name": request.user.name,
"given_name": request.user.name,
"family_name": "",
"preferred_username": request.user.username,
"nickname": request.user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in request.user.ak_groups.all()],
}
"""
class ScopeMappingManager(ObjectManager):
"""OAuth2 Provider managed objects"""
def reconcile(self):
return [
EnsureExists(
ScopeMapping,
"goauthentik.io/providers/oauth2/scope-openid",
name="authentik default OAuth Mapping: OpenID 'openid'",
scope_name="openid",
expression=SCOPE_OPENID_EXPRESSION,
),
EnsureExists(
ScopeMapping,
"goauthentik.io/providers/oauth2/scope-email",
name="authentik default OAuth Mapping: OpenID 'email'",
scope_name="email",
description="Email address",
expression=SCOPE_EMAIL_EXPRESSION,
),
EnsureExists(
ScopeMapping,
"goauthentik.io/providers/oauth2/scope-profile",
name="authentik default OAuth Mapping: OpenID 'profile'",
scope_name="profile",
description="General Profile Information",
expression=SCOPE_PROFILE_EXPRESSION,
),
]

View file

@ -5,7 +5,7 @@ from django.test import RequestFactory
from django.urls import reverse
from jwt import decode
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id, generate_key
@ -24,9 +24,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenClientCredentials(OAuthTestCase):
"""Test token (client_credentials) view"""
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def setUp(self) -> None:
super().setUp()
ObjectManager().run()
self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create(
name="test",

View file

@ -6,7 +6,7 @@ from django.test import RequestFactory
from django.urls import reverse
from jwt import decode
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.lib.generators import generate_id, generate_key
@ -26,9 +26,9 @@ from authentik.sources.oauth.models import OAuthSource
class TestTokenClientCredentialsJWTSource(OAuthTestCase):
"""Test token (client_credentials, with JWT) view"""
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def setUp(self) -> None:
super().setUp()
ObjectManager().run()
self.factory = RequestFactory()
self.cert = create_test_cert()

View file

@ -4,7 +4,7 @@ from dataclasses import asdict
from django.urls import reverse
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.events.models import Event, EventAction
@ -16,9 +16,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestUserinfo(OAuthTestCase):
"""Test token view"""
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def setUp(self) -> None:
super().setUp()
ObjectManager().run()
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),

View file

@ -1,6 +1,4 @@
"""authentik Proxy app"""
from importlib import import_module
from django.apps import AppConfig
@ -10,6 +8,3 @@ class AuthentikProviderProxyConfig(AppConfig):
name = "authentik.providers.proxy"
label = "authentik_providers_proxy"
verbose_name = "authentik Providers.Proxy"
def ready(self) -> None:
import_module("authentik.providers.proxy.managed")

View file

@ -1,29 +0,0 @@
"""OAuth2 Provider managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.providers.oauth2.models import ScopeMapping
from authentik.providers.proxy.models import SCOPE_AK_PROXY
SCOPE_AK_PROXY_EXPRESSION = """
# This mapping is used by the authentik proxy. It passes extra user attributes,
# which are used for example for the HTTP-Basic Authentication mapping.
return {
"ak_proxy": {
"user_attributes": request.user.group_attributes(request),
"is_superuser": request.user.is_superuser,
}
}"""
class ProxyScopeMappingManager(ObjectManager):
"""OAuth2 Provider managed objects"""
def reconcile(self):
return [
EnsureExists(
ScopeMapping,
"goauthentik.io/providers/proxy/scope-proxy",
name="authentik default OAuth Mapping: Proxy outpost",
scope_name=SCOPE_AK_PROXY,
expression=SCOPE_AK_PROXY_EXPRESSION,
),
]

View file

@ -1,5 +1,4 @@
"""authentik SAML IdP app config"""
from importlib import import_module
from django.apps import AppConfig
@ -11,6 +10,3 @@ class AuthentikProviderSAMLConfig(AppConfig):
label = "authentik_providers_saml"
verbose_name = "authentik Providers.SAML"
mountpoint = "application/saml/"
def ready(self) -> None:
import_module("authentik.providers.saml.managed")

View file

@ -1,74 +0,0 @@
"""SAML Provider managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.providers.saml.models import SAMLPropertyMapping
GROUP_EXPRESSION = """
for group in request.user.ak_groups.all():
yield group.name
"""
class SAMLProviderManager(ObjectManager):
"""SAML Provider managed objects"""
def reconcile(self):
return [
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/upn",
name="authentik default SAML Mapping: UPN",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
expression="return request.user.attributes.get('upn', request.user.email)",
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/name",
name="authentik default SAML Mapping: Name",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
expression="return request.user.name",
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/email",
name="authentik default SAML Mapping: Email",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
expression="return request.user.email",
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/username",
name="authentik default SAML Mapping: Username",
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
expression="return request.user.username",
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/uid",
name="authentik default SAML Mapping: User ID",
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
expression="return request.user.pk",
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/groups",
name="authentik default SAML Mapping: Groups",
saml_name="http://schemas.xmlsoap.org/claims/Group",
expression=GROUP_EXPRESSION,
friendly_name="",
),
EnsureExists(
SAMLPropertyMapping,
"goauthentik.io/providers/saml/ms-windowsaccountname",
name="authentik default SAML Mapping: WindowsAccountname (Username)",
saml_name=(
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
),
expression="return request.user.username",
friendly_name="",
),
]

View file

@ -4,7 +4,7 @@ from base64 import b64encode
from django.http.request import QueryDict
from django.test import RequestFactory, TestCase
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
@ -74,8 +74,8 @@ qNAZMq1DqpibfCBg
class TestAuthNRequest(TestCase):
"""Test AuthN Request generator and parser"""
@apply_blueprint("blueprints/system/providers-saml.yaml")
def setUp(self):
ObjectManager().run()
cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),

View file

@ -4,7 +4,7 @@ from base64 import b64encode
from django.test import RequestFactory, TestCase
from lxml import etree # nosec
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.lib.tests.utils import get_request
from authentik.lib.xml import lxml_from_string
@ -18,8 +18,8 @@ from authentik.sources.saml.processors.request import RequestProcessor
class TestSchema(TestCase):
"""Test Requests and Responses against schema"""
@apply_blueprint("blueprints/system/providers-saml.yaml")
def setUp(self):
ObjectManager().run()
cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),

View file

@ -58,6 +58,7 @@ def task_prerun_hook(task_id: str, task, *args, **kwargs):
@task_postrun.connect
def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs):
"""Log task_id on worker"""
CTX_TASK_ID.set(...)
LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state)
@ -69,6 +70,7 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs):
from authentik.events.models import Event, EventAction
LOGGER.warning("Task failure", exc=exception)
CTX_TASK_ID.set(...)
if before_send({}, {"exc_info": (None, exception, None)}) is not None:
Event.new(EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception)).save()
@ -76,7 +78,6 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs):
def _get_startup_tasks() -> list[Callable]:
"""Get all tasks to be run on startup"""
from authentik.admin.tasks import clear_update_notifications
from authentik.blueprints.tasks import managed_reconcile
from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection
from authentik.providers.proxy.tasks import proxy_set_defaults
@ -85,7 +86,6 @@ def _get_startup_tasks() -> list[Callable]:
outpost_local_connection,
outpost_controller_all,
proxy_set_defaults,
managed_reconcile,
]

View file

@ -1,16 +1,15 @@
"""authentik ldap source config"""
from importlib import import_module
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikSourceLDAPConfig(AppConfig):
class AuthentikSourceLDAPConfig(ManagedAppConfig):
"""Authentik ldap app config"""
name = "authentik.sources.ldap"
label = "authentik_sources_ldap"
verbose_name = "authentik Sources.LDAP"
default = True
def ready(self):
import_module("authentik.sources.ldap.signals")
import_module("authentik.sources.ldap.managed")
def reconcile_load_sources_ldap_signals(self):
"""Load sources.ldap signals"""
self.import_module("authentik.sources.ldap.signals")

View file

@ -1,69 +0,0 @@
"""LDAP Source managed objects"""
from authentik.blueprints.manager import EnsureExists, ObjectManager
from authentik.sources.ldap.models import LDAPPropertyMapping
class LDAPProviderManager(ObjectManager):
"""LDAP Source managed objects"""
def reconcile(self):
return [
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/default-name",
name="authentik default LDAP Mapping: Name",
object_field="name",
expression="return ldap.get('name')",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/default-mail",
name="authentik default LDAP Mapping: mail",
object_field="email",
expression="return ldap.get('mail')",
),
# Active Directory-specific mappings
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-samaccountname",
name="authentik default Active Directory Mapping: sAMAccountName",
object_field="username",
expression="return ldap.get('sAMAccountName')",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-userprincipalname",
name="authentik default Active Directory Mapping: userPrincipalName",
object_field="attributes.upn",
expression="return list_flatten(ldap.get('userPrincipalName'))",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-givenName",
name="authentik default Active Directory Mapping: givenName",
object_field="attributes.givenName",
expression="return list_flatten(ldap.get('givenName'))",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-sn",
name="authentik default Active Directory Mapping: sn",
object_field="attributes.sn",
expression="return list_flatten(ldap.get('sn'))",
),
# OpenLDAP specific mappings
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/openldap-uid",
name="authentik default OpenLDAP Mapping: uid",
object_field="username",
expression="return ldap.get('uid')",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/openldap-cn",
name="authentik default OpenLDAP Mapping: cn",
object_field="name",
expression="return ldap.get('cn')",
),
]

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock, PropertyMock, patch
from django.db.models import Q
from django.test import TestCase
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.models import User
from authentik.lib.generators import generate_key
from authentik.sources.ldap.auth import LDAPBackend
@ -19,8 +19,8 @@ LDAP_PASSWORD = generate_key()
class LDAPSyncTests(TestCase):
"""LDAP Sync tests"""
@apply_blueprint("blueprints/system/sources-ldap.yaml")
def setUp(self):
ObjectManager().run()
self.source = LDAPSource.objects.create(
name="ldap",
slug="ldap",

View file

@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch
from django.db.models import Q
from django.test import TestCase
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints import apply_blueprint
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
@ -23,8 +23,8 @@ LDAP_PASSWORD = generate_key()
class LDAPSyncTests(TestCase):
"""LDAP Sync tests"""
@apply_blueprint("blueprints/system/sources-ldap.yaml")
def setUp(self):
ObjectManager().run()
self.source: LDAPSource = LDAPSource.objects.create(
name="ldap",
slug="ldap",

View file

@ -1,9 +1,8 @@
"""authentik oauth_client config"""
from importlib import import_module
from django.apps import AppConfig
from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig
LOGGER = get_logger()
AUTHENTIK_SOURCES_OAUTH_TYPES = [
@ -21,18 +20,19 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
]
class AuthentikSourceOAuthConfig(AppConfig):
class AuthentikSourceOAuthConfig(ManagedAppConfig):
"""authentik source.oauth config"""
name = "authentik.sources.oauth"
label = "authentik_sources_oauth"
verbose_name = "authentik Sources.OAuth"
mountpoint = "source/oauth/"
default = True
def ready(self):
def reconcile_sources_loaded(self):
"""Load source_types from config file"""
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
try:
import_module(source_type)
self.import_module(source_type)
except ImportError as exc:
LOGGER.warning("Failed to load OAuth Source", exc=exc)

View file

@ -1,17 +1,16 @@
"""Authentik SAML app config"""
from importlib import import_module
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikSourceSAMLConfig(AppConfig):
class AuthentikSourceSAMLConfig(ManagedAppConfig):
"""authentik saml source app config"""
name = "authentik.sources.saml"
label = "authentik_sources_saml"
verbose_name = "authentik Sources.SAML"
mountpoint = "source/saml/"
default = True
def ready(self):
import_module("authentik.sources.saml.signals")
def reconcile_load_sources_saml_signals(self):
"""Load sources.saml signals"""
self.import_module("authentik.sources.saml.signals")

View file

@ -1,15 +1,15 @@
"""Authenticator Static stage"""
from importlib import import_module
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikStageAuthenticatorStaticConfig(AppConfig):
class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig):
"""Authenticator Static stage"""
name = "authentik.stages.authenticator_static"
label = "authentik_stages_authenticator_static"
verbose_name = "authentik Stages.Authenticator.Static"
default = True
def ready(self):
import_module("authentik.stages.authenticator_static.signals")
def reconcile_load_stages_authenticator_static_signals(self):
"""Load stages.authenticator_static signals"""
self.import_module("authentik.stages.authenticator_static.signals")

View file

@ -1,30 +1,26 @@
"""authentik email stage config"""
from importlib import import_module
from django.apps import AppConfig
from django.db import ProgrammingError
from django.template.exceptions import TemplateDoesNotExist
from django.template.loader import get_template
from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig
LOGGER = get_logger()
class AuthentikStageEmailConfig(AppConfig):
class AuthentikStageEmailConfig(ManagedAppConfig):
"""authentik email stage config"""
name = "authentik.stages.email"
label = "authentik_stages_email"
verbose_name = "authentik Stages.Email"
default = True
def ready(self):
import_module("authentik.stages.email.tasks")
try:
self.validate_stage_templates()
except ProgrammingError:
pass
def reconcile_load_stages_emails_tasks(self):
"""Load stages.emails tasks"""
self.import_module("authentik.stages.email.tasks")
def validate_stage_templates(self):
def reconcile_stage_templates_valid(self):
"""Ensure all stage's templates actually exist"""
from authentik.events.models import Event, EventAction
from authentik.stages.email.models import EmailStage, EmailTemplates

View file

@ -27,7 +27,7 @@ entries:
expression: |
# Check if we''ve not been given a username by the external IdP
# and trigger the enrollment flow
return ''username'' not in context.get(''prompt_data'', {})
return 'username' not in context.get('prompt_data', {})
meta_model_name: authentik_policies_expression.expressionpolicy
identifiers:
name: default-source-enrollment-if-username

View file

@ -5,7 +5,7 @@ entries:
layout: stacked
name: Pre-Authentication
policy_engine_mode: any
title: ''
title: Pre-authentication
identifiers:
slug: default-source-pre-authentication
model: authentik_flows.flow

View file

@ -3,9 +3,9 @@ entries:
compatibility_mode: false
designation: stage_configuration
layout: stacked
name: Update your info
name: User settings
policy_engine_mode: any
title: ''
title: Update your info
identifiers:
slug: default-user-settings-flow
model: authentik_flows.flow
@ -108,9 +108,9 @@ entries:
return True
meta_model_name: authentik_policies_expression.expressionpolicy
name: default-user-settings-authorization
identifiers:
name: default-user-settings-authorization
id: default-user-settings-authorization
model: authentik_policies_expression.expressionpolicy
- attrs:
create_users_as_inactive: false

View file

@ -76,7 +76,6 @@ entries:
- !KeyOf prompt-field-password
- !KeyOf prompt-field-password-repeat
- identifiers:
pk: !KeyOf default-enrollment-user-login
name: default-enrollment-user-login
id: default-enrollment-user-login
model: authentik_stages_user_login.userloginstage

View file

@ -39,7 +39,6 @@ entries:
model: authentik_stages_authenticator_validate.AuthenticatorValidateStage
attrs: {}
- identifiers:
pk: !KeyOf default-authentication-password
name: default-authentication-password
id: default-authentication-password
model: authentik_stages_password.passwordstage

View file

@ -93,7 +93,7 @@ entries:
session_duration: seconds=0
- identifiers:
name: Change your password
name: stages-prompt-password
id: stages-prompt-password
model: authentik_stages_prompt.promptstage
attrs:
fields:

View file

@ -0,0 +1,44 @@
version: 1
entries:
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-openid
model: authentik_providers_oauth2.ScopeMapping
attrs:
name: "authentik default OAuth Mapping: OpenID 'openid'"
scope_name: openid
expression: |
# This scope is required by the OpenID-spec, and must as such exist in authentik.
# The scope by itself does not grant any information
return {}
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-email
model: authentik_providers_oauth2.ScopeMapping
attrs:
name: "authentik default OAuth Mapping: OpenID 'email'"
scope_name: email
description: "Email address"
expression: |
return {
"email": request.user.email,
"email_verified": True
}
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-profile
model: authentik_providers_oauth2.ScopeMapping
attrs:
name: "authentik default OAuth Mapping: OpenID 'profile'"
scope_name: profile
description: "General Profile Information"
expression: |
return {
# Because authentik only saves the user's full name, and has no concept of first and last names,
# the full name is used as given name.
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
"name": request.user.name,
"given_name": request.user.name,
"family_name": "",
"preferred_username": request.user.username,
"nickname": request.user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in request.user.ak_groups.all()],
}

View file

@ -0,0 +1,17 @@
version: 1
entries:
- identifiers:
managed: goauthentik.io/providers/proxy/scope-proxy
model: authentik_providers_oauth2.ScopeMapping
attrs:
name: "authentik default OAuth Mapping: Proxy outpost"
scope_name: ak_proxy
expression: |
# This mapping is used by the authentik proxy. It passes extra user attributes,
# which are used for example for the HTTP-Basic Authentication mapping.
return {
"ak_proxy": {
"user_attributes": request.user.group_attributes(request),
"is_superuser": request.user.is_superuser,
}
}

View file

@ -0,0 +1,59 @@
version: 1
entries:
- identifiers:
managed: goauthentik.io/providers/saml/upn
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: UPN"
saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
expression: |
return request.user.attributes.get('upn', request.user.email)
- identifiers:
managed: goauthentik.io/providers/saml/name
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: Name"
saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
expression: |
return request.user.name
- identifiers:
managed: goauthentik.io/providers/saml/email
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: Email"
saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
expression: |
return request.user.email
- identifiers:
managed: goauthentik.io/providers/saml/username
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: Username"
saml_name: "http://schemas.goauthentik.io/2021/02/saml/username"
expression: |
return request.user.username
- identifiers:
managed: goauthentik.io/providers/saml/uid
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: User ID"
saml_name: "http://schemas.goauthentik.io/2021/02/saml/uid"
expression: |
return request.user.pk
- identifiers:
managed: goauthentik.io/providers/saml/groups
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: Groups"
saml_name: "http://schemas.xmlsoap.org/claims/Group"
expression: |
for group in request.user.ak_groups.all():
yield group.name
- identifiers:
managed: goauthentik.io/providers/saml/ms-windowsaccountname
model: authentik_providers_saml.SAMLPropertyMapping
attrs:
name: "authentik default SAML Mapping: WindowsAccountname (Username)"
saml_name: "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
expression: |
return request.user.username

View file

@ -0,0 +1,68 @@
version: 1
entries:
- identifiers:
managed: goauthentik.io/sources/ldap/default-name
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default LDAP Mapping: Name"
object_field: "name"
expression: |
return ldap.get('name')
- identifiers:
managed: goauthentik.io/sources/ldap/default-mail
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default LDAP Mapping: mail"
object_field: "email"
expression: |
return ldap.get('mail')
# ActiveDirectory-specific mappings
- identifiers:
managed: goauthentik.io/sources/ldap/ms-samaccountname
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default Active Directory Mapping: sAMAccountName"
object_field: "username"
expression: |
return ldap.get('sAMAccountName')
- identifiers:
managed: goauthentik.io/sources/ldap/ms-userprincipalname
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default Active Directory Mapping: userPrincipalName"
object_field: "attributes.upn"
expression: |
return list_flatten(ldap.get('userPrincipalName'))
- identifiers:
managed: goauthentik.io/sources/ldap/ms-givenName
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default Active Directory Mapping: givenName"
object_field: "attributes.givenName"
expression: |
return list_flatten(ldap.get('givenName'))
- identifiers:
managed: goauthentik.io/sources/ldap/ms-sn
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default Active Directory Mapping: sn"
object_field: "attributes.sn"
expression: |
return list_flatten(ldap.get('sn'))
# OpenLDAP specific mappings
- identifiers:
managed: goauthentik.io/sources/ldap/openldap-uid
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default OpenLDAP Mapping: uid"
object_field: "username"
expression: |
return ldap.get('uid')
- identifiers:
managed: goauthentik.io/sources/ldap/openldap-cn
model: authentik_sources_ldap.LDAPPropertyMapping
attrs:
name: "authentik default OpenLDAP Mapping: cn"
object_field: "name"
expression: |
return ldap.get('cn')

View file

@ -20866,6 +20866,11 @@ components:
type: object
description: Info about a single blueprint instance file
properties:
pk:
type: string
format: uuid
readOnly: true
title: Instance uuid
name:
type: string
path:
@ -20877,15 +20882,26 @@ components:
type: string
format: date-time
readOnly: true
last_applied_hash:
type: string
readOnly: true
status:
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
enabled:
type: boolean
managed_models:
type: array
items:
type: string
readOnly: true
required:
- context
- last_applied
- last_applied_hash
- managed_models
- name
- path
- pk
- status
BlueprintInstanceRequest:
type: object
@ -20914,6 +20930,7 @@ components:
- successful
- warning
- error
- orphaned
- unknown
type: string
Cache:

View file

@ -0,0 +1,26 @@
"""Generate config for development"""
from yaml import safe_dump
from authentik.lib.generators import generate_id
with open("local.env.yml", "w") as _config:
safe_dump(
{
"log_level": "debug",
"secret_key": generate_id(),
"postgresql": {
"user": "postgres",
},
"outposts": {
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
"blueprint_locations": ["./blueprints"],
},
"web": {
"outpost_port_offset": 100,
},
"cert_discovery_dir": "./certs",
"geoip": "tests/GeoLite2-City-Test.mmdb",
},
_config,
default_flow_style=False,
)

View file

@ -13,11 +13,11 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
from authentik.flows.models import Flow, FlowStageBinding
from authentik.blueprints import apply_blueprint
from authentik.flows.models import Flow
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
from tests.e2e.utils import SeleniumTestCase, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -25,18 +25,16 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test flow with otp stages"""
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_totp_validate(self):
"""test flow with otp stages"""
sleep(1)
# Setup TOTP Device
device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6)
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
FlowStageBinding.objects.create(
target=flow, order=30, stage=AuthenticatorValidateStage.objects.create()
)
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
@ -47,16 +45,17 @@ class TestFlowsAuthenticator(SeleniumTestCase):
flow_executor = self.get_shadow_root("ak-flow-executor")
validation_stage = self.get_shadow_root("ak-stage-authenticator-validate", flow_executor)
code_stage = self.get_shadow_root("ak-stage-authenticator-validate-code", validation_stage)
code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(totp.token())
code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_authenticator_totp", "0006_default_setup_flow")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint("blueprints/default/20-flow-default-authenticator-totp-setup.yaml")
def test_totp_setup(self):
"""test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
@ -98,9 +97,11 @@ class TestFlowsAuthenticator(SeleniumTestCase):
self.assertTrue(TOTPDevice.objects.filter(user=self.user, confirmed=True).exists())
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_authenticator_static", "0005_default_setup_flow")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint("blueprints/default/20-flow-default-authenticator-static-setup.yaml")
def test_static_setup(self):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")

View file

@ -9,6 +9,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
from authentik.blueprints import apply_blueprint
from authentik.core.models import User
from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding
@ -18,7 +19,7 @@ from authentik.stages.identification.models import IdentificationStage
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.user_login.models import UserLoginStage
from authentik.stages.user_write.models import UserWriteStage
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
from tests.e2e.utils import SeleniumTestCase, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -39,8 +40,10 @@ class TestFlowsEnroll(SeleniumTestCase):
}
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
# First stage fields
@ -103,8 +106,10 @@ class TestFlowsEnroll(SeleniumTestCase):
self.assertEqual(user.email, "foo@bar.baz")
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_enroll_email(self):
"""Test enroll with Email verification"""
# First stage fields

View file

@ -2,7 +2,8 @@
from sys import platform
from unittest.case import skipUnless
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
from authentik.blueprints import apply_blueprint
from tests.e2e.utils import SeleniumTestCase, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -10,8 +11,10 @@ class TestFlowsLogin(SeleniumTestCase):
"""test default login flow"""
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_login(self):
"""test default login flow"""
self.driver.get(

View file

@ -5,11 +5,12 @@ from unittest.case import skipUnless
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from authentik.blueprints import apply_blueprint
from authentik.core.models import User
from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.generators import generate_key
from authentik.stages.password.models import PasswordStage
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
from tests.e2e.utils import SeleniumTestCase, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -17,9 +18,11 @@ class TestFlowsStageSetup(SeleniumTestCase):
"""test stage setup flows"""
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_password", "0002_passwordstage_change_flow")
@apply_blueprint("blueprints/default/0-flow-password-change.yaml")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_password_change(self):
"""test password change flow"""
# Ensure that password stage has change_flow set

View file

@ -10,13 +10,14 @@ from guardian.shortcuts import get_anonymous_user
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application, User
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -81,8 +82,10 @@ class TestProviderLDAP(SeleniumTestCase):
return outpost
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_success(self):
"""Test simple bind"""
self._prepare()
@ -106,8 +109,10 @@ class TestProviderLDAP(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_success_ssl(self):
"""Test simple bind with ssl"""
self._prepare()
@ -131,8 +136,10 @@ class TestProviderLDAP(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_fail(self):
"""Test simple bind (failed)"""
self._prepare()
@ -154,8 +161,11 @@ class TestProviderLDAP(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@reconcile_app("authentik_outposts")
def test_ldap_bind_search(self):
"""Test simple bind + search"""
outpost = self._prepare()

View file

@ -8,13 +8,14 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -56,10 +57,18 @@ class TestProviderOAuth2Github(SeleniumTestCase):
}
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_consent_implied(self):
"""test OAuth Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects
@ -104,10 +113,18 @@ class TestProviderOAuth2Github(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_consent_explicit(self):
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
# Bootstrap all needed objects
@ -171,10 +188,15 @@ class TestProviderOAuth2Github(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_denied(self):
"""test OAuth Provider flow (default authorization flow, denied)"""
# Bootstrap all needed objects

View file

@ -8,6 +8,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow
@ -20,7 +21,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OPENID_PROFILE,
)
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -65,10 +66,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
}
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -106,11 +115,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
@ -161,11 +177,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_logout(self):
"""test OpenID Provider flow with logout"""
sleep(1)
@ -225,11 +248,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.ID, "logout").click()
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
@ -298,10 +328,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)

View file

@ -10,6 +10,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow
@ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OPENID_PROFILE,
)
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -64,10 +65,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
sleep(1)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -105,11 +111,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
@ -155,11 +166,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
self.assertEqual(body["UserInfo"]["email"], self.user.email)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
@ -220,10 +236,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
self.assertEqual(body["UserInfo"]["email"], self.user.email)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)

View file

@ -10,6 +10,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow
@ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OPENID_PROFILE,
)
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -64,10 +65,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
sleep(1)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -105,11 +111,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
@ -150,11 +161,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
self.assertEqual(body["profile"]["email"], self.user.email)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
@ -211,10 +227,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
self.assertEqual(body["profile"]["email"], self.user.email)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)

View file

@ -11,12 +11,13 @@ from docker.models.containers import Container
from selenium.webdriver.common.by import By
from authentik import __version__
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.flows.models import Flow
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
from authentik.outposts.tasks import outpost_local_connection
from authentik.providers.proxy.models import ProxyProvider
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -53,11 +54,19 @@ class TestProviderProxy(SeleniumTestCase):
return container
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-oauth2.yaml",
"blueprints/system/providers-proxy.yaml",
)
@reconcile_app("authentik_crypto")
def test_proxy_simple(self):
"""Test simple outpost setup with single provider"""
# set additionalHeaders to test later
@ -116,11 +125,15 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
"""Test Proxy connectivity over websockets"""
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@reconcile_app("authentik_crypto")
def test_proxy_connectivity(self):
"""Test proxy connectivity over websocket"""
outpost_local_connection()

View file

@ -10,6 +10,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow
@ -17,7 +18,7 @@ from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
from authentik.sources.saml.processors.constants import SAML_BINDING_POST
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -63,11 +64,18 @@ class TestProviderSAML(SeleniumTestCase):
sleep(1)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
@ -125,11 +133,18 @@ class TestProviderSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_explicit(self):
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
# Bootstrap all needed objects
@ -202,11 +217,18 @@ class TestProviderSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_explicit_post(self):
"""test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)"""
# Bootstrap all needed objects
@ -279,11 +301,18 @@ class TestProviderSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_idp_initiated_implicit(self):
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
@ -347,11 +376,18 @@ class TestProviderSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
# Bootstrap all needed objects

View file

@ -13,6 +13,7 @@ from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
from yaml import safe_dump
from authentik.blueprints import apply_blueprint
from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key
@ -20,7 +21,7 @@ from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.stages.identification.models import IdentificationStage
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, retry
CONFIG_PATH = "/tmp/dex.yml" # nosec
@ -141,11 +142,19 @@ class TestSourceOAuth2(SeleniumTestCase):
ident_stage.save()
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-source-authentication.yaml",
"blueprints/default/20-flow-default-source-enrollment.yaml",
"blueprints/default/20-flow-default-source-pre-authentication.yaml",
)
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()
@ -190,11 +199,14 @@ class TestSourceOAuth2(SeleniumTestCase):
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml",
"blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml",
)
def test_oauth_enroll_auth(self):
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
self.test_oauth_enroll()
@ -279,11 +291,15 @@ class TestSourceOAuth1(SeleniumTestCase):
ident_stage.save()
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-source-authentication.yaml",
"blueprints/default/20-flow-default-source-enrollment.yaml",
"blueprints/default/20-flow-default-source-pre-authentication.yaml",
)
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()

View file

@ -11,12 +11,13 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
from authentik.blueprints import apply_blueprint
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
from authentik.stages.identification.models import IdentificationStage
from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry
from tests.e2e.utils import SeleniumTestCase, retry
IDP_CERT = """-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
@ -94,12 +95,15 @@ class TestSourceSAML(SeleniumTestCase):
}
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-source-authentication.yaml",
"blueprints/default/20-flow-default-source-enrollment.yaml",
"blueprints/default/20-flow-default-source-pre-authentication.yaml",
)
def test_idp_redirect(self):
"""test SAML Source With redirect binding"""
# Bootstrap all needed objects
@ -161,12 +165,15 @@ class TestSourceSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-source-authentication.yaml",
"blueprints/default/20-flow-default-source-enrollment.yaml",
"blueprints/default/20-flow-default-source-pre-authentication.yaml",
)
def test_idp_post(self):
"""test SAML Source With post binding"""
# Bootstrap all needed objects
@ -241,12 +248,15 @@ class TestSourceSAML(SeleniumTestCase):
)
@retry()
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow")
@object_manager
@apply_blueprint(
"blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"blueprints/default/20-flow-default-source-authentication.yaml",
"blueprints/default/20-flow-default-source-enrollment.yaml",
"blueprints/default/20-flow-default-source-pre-authentication.yaml",
)
def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects

View file

@ -10,7 +10,6 @@ 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.db.migrations.operations.special import RunPython
from django.test.testcases import TransactionTestCase
from django.urls import reverse
from docker import DockerClient, from_env
@ -25,7 +24,7 @@ from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from structlog.stdlib import get_logger
from authentik.blueprints.manager import ObjectManager
from authentik.blueprints.manager import ManagedAppConfig
from authentik.core.api.users import UserSerializer
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
@ -193,37 +192,22 @@ def get_loader():
return MigrationLoader(connection)
def apply_migration(app_name: str, migration_name: str):
"""Re-apply migrations that create objects using RunPython before test cases"""
def reconcile_app(app_name: str):
"""Re-reconcile AppConfig methods"""
def wrapper_outter(func: Callable):
"""Retry test multiple times"""
def wrapper_outer(func: Callable):
"""Re-reconcile AppConfig methods"""
@wraps(func)
def wrapper(self: TransactionTestCase, *args, **kwargs):
migration = get_loader().get_migration(app_name, migration_name)
with connection.schema_editor() as schema_editor:
for operation in migration.operations:
if not isinstance(operation, RunPython):
continue
operation.code(apps, schema_editor)
config = apps.get_app_config(app_name)
if isinstance(config, ManagedAppConfig):
config.reconcile()
return func(self, *args, **kwargs)
return wrapper
return wrapper_outter
def object_manager(func: Callable):
"""Run objectmanager before a test function"""
@wraps(func)
def wrapper(*args, **kwargs):
"""Run objectmanager before a test function"""
ObjectManager().run()
return func(*args, **kwargs)
return wrapper
return wrapper_outer
def retry(max_retires=RETRIES, exceptions=None):

View file

@ -23,16 +23,7 @@ poetry shell # Creates a python virtualenv, and activates it in a new shell
poetry install # Install all required dependencies, including development dependencies
```
To configure authentik to use the local databases, create a file in the authentik directory called `local.env.yml`, with the following contents
```yaml
debug: true
postgresql:
user: postgres
log_level: debug
secret_key: "A long key you can generate with `pwgen 40 1` for example"
```
To configure authentik to use the local databases, we need a local config file. This file can be generated by running `make gen-dev-config`.
To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream.