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

View File

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

View File

@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer 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 from authentik.outposts.models import Outpost

View File

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

View File

@ -1,11 +1,11 @@
"""test admin api""" """test admin api"""
from json import loads from json import loads
from django.apps import apps
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from authentik import __version__ from authentik import __version__
from authentik.blueprints.tasks import managed_reconcile
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models from authentik.core.tasks import clean_expired_models
from authentik.events.monitored_tasks import TaskResultStatus from authentik.events.monitored_tasks import TaskResultStatus
@ -95,7 +95,6 @@ class TestAdminAPI(TestCase):
def test_system(self): def test_system(self):
"""Test system API""" """Test system API"""
# pyright: reportGeneralTypeIssues=false apps.get_app_config("authentik_outposts").reconcile_embedded_outpost()
managed_reconcile() # pylint: disable=no-value-for-parameter
response = self.client.get(reverse("authentik_api:admin_system")) response = self.client.get(reverse("authentik_api:admin_system"))
self.assertEqual(response.status_code, 200) 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]: def token_secret_key(value: str) -> Optional[User]:
"""Check if the token is the secret key """Check if the token is the secret key
and return the service account for the managed outpost""" 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: if value != settings.SECRET_KEY:
return None return None

View File

@ -1,6 +1,7 @@
"""Test API Authentication""" """Test API Authentication"""
from base64 import b64encode from base64 import b64encode
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from guardian.shortcuts import get_anonymous_user 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.models import USER_ATTRIBUTE_SA, Token, TokenIntents
from authentik.core.tests.utils import create_test_flow from authentik.core.tests.utils import create_test_flow
from authentik.lib.generators import generate_id 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.constants import SCOPE_AUTHENTIK_API
from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken
@ -44,7 +44,7 @@ class TestAPIAuth(TestCase):
with self.assertRaises(AuthenticationFailed): with self.assertRaises(AuthenticationFailed):
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) 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()) user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True) 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 model = BlueprintInstance
fields = [ fields = [
"pk",
"name", "name",
"path", "path",
"context", "context",
"last_applied", "last_applied",
"last_applied_hash",
"status", "status",
"enabled", "enabled",
"managed_models",
] ]
extra_kwargs = {
"last_applied": {"read_only": True},
"last_applied_hash": {"read_only": True},
"managed_models": {"read_only": True},
}
class BlueprintInstanceViewSet(ModelViewSet): class BlueprintInstanceViewSet(ModelViewSet):

View File

@ -1,15 +1,22 @@
"""authentik Blueprints app""" """authentik Blueprints app"""
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikBlueprintsConfig(AppConfig): class AuthentikBlueprintsConfig(ManagedAppConfig):
"""authentik Blueprints app""" """authentik Blueprints app"""
name = "authentik.blueprints" name = "authentik.blueprints"
label = "authentik_blueprints" label = "authentik_blueprints"
verbose_name = "authentik Blueprints" verbose_name = "authentik Blueprints"
default = True
def ready(self) -> None: def reconcile_load_blueprints_v1_tasks(self):
from authentik.blueprints.tasks import managed_reconcile """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", []): for blueprint_path in options.get("blueprints", []):
with open(blueprint_path, "r", encoding="utf8") as blueprint_file: with open(blueprint_path, "r", encoding="utf8") as blueprint_file:
importer = Importer(blueprint_file.read()) importer = Importer(blueprint_file.read())
valid = importer.validate() valid, logs = importer.validate()
if not valid: if not valid:
raise ValueError("blueprint invalid") raise ValueError(f"blueprint invalid: {logs}")
importer.apply() importer.apply()
def add_arguments(self, parser): def add_arguments(self, parser):

View File

@ -1,70 +1,37 @@
"""Managed objects manager""" """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 structlog.stdlib import get_logger
from authentik.blueprints.models import ManagedModel
LOGGER = get_logger() LOGGER = get_logger()
class EnsureOp: class ManagedAppConfig(AppConfig):
"""Ensure operation, executed as part of an ObjectManager run""" """Basic reconciliation logic for apps"""
_obj: type[ManagedModel] def ready(self) -> None:
_managed_uid: str self.reconcile()
_kwargs: dict return super().ready()
def __init__(self, obj: type[ManagedModel], managed_uid: str, **kwargs) -> None: def import_module(self, path: str):
self._obj = obj """Load module"""
self._managed_uid = managed_uid import_module(path)
self._kwargs = kwargs
def run(self): def reconcile(self) -> None:
"""Do the actual ensure action""" """reconcile ourselves"""
raise NotImplementedError prefix = "reconcile_"
for meth_name in dir(self):
meth = getattr(self, meth_name)
class EnsureExists(EnsureOp): if not ismethod(meth):
"""Ensure object exists, with kwargs as given values""" continue
if not meth_name.startswith(prefix):
created_callback: Optional[Callable] continue
name = meth_name.replace(prefix, "")
def __init__( try:
self, meth()
obj: type[ManagedModel], LOGGER.debug("Successfully reconciled", name=name)
managed_uid: str, except (ProgrammingError, DatabaseError) as exc:
created_callback: Optional[Callable] = None, LOGGER.debug("Failed to run reconcile", name=name, exc=exc)
**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

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 uuid
import django.contrib.postgres.fields import django.contrib.postgres.fields
from django.apps.registry import Apps
from django.db import migrations, models 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): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [("authentik_flows", "0001_initial")]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
@ -38,6 +59,7 @@ class Migration(migrations.Migration):
("path", models.TextField()), ("path", models.TextField()),
("context", models.JSONField()), ("context", models.JSONField()),
("last_applied", models.DateTimeField(auto_now=True)), ("last_applied", models.DateTimeField(auto_now=True)),
("last_applied_hash", models.TextField()),
( (
"status", "status",
models.TextField( models.TextField(
@ -45,6 +67,7 @@ class Migration(migrations.Migration):
("successful", "Successful"), ("successful", "Successful"),
("warning", "Warning"), ("warning", "Warning"),
("error", "Error"), ("error", "Error"),
("orphaned", "Orphaned"),
("unknown", "Unknown"), ("unknown", "Unknown"),
] ]
), ),
@ -63,4 +86,5 @@ class Migration(migrations.Migration):
"unique_together": {("name", "path")}, "unique_together": {("name", "path")},
}, },
), ),
migrations.RunPython(migration_blueprint_import),
] ]

View File

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

View File

@ -1,17 +1,12 @@
"""managed Settings""" """blueprint Settings"""
from celery.schedules import crontab from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"blueprints_reconcile": { "blueprints_v1_discover": {
"task": "authentik.blueprints.tasks.managed_reconcile", "task": "authentik.blueprints.v1.tasks.blueprints_discover",
"schedule": crontab(minute=fqdn_rand("managed_reconcile"), hour="*/4"), "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},
"blueprints_config_file_discovery": {
"task": "authentik.blueprints.tasks.config_file_discovery",
"schedule": crontab(minute=fqdn_rand("config_file_discovery"), hour="*"),
"options": {"queue": "authentik_scheduled"}, "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): def test_bundle_invalid_format(self):
"""Test bundle with invalid format""" """Test bundle with invalid format"""
importer = Importer('{"version": 3}') importer = Importer('{"version": 3}')
self.assertFalse(importer.validate()) self.assertFalse(importer.validate()[0])
importer = Importer( importer = Importer(
( (
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},' '{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
'"model": "authentik_core.User"}]}' '"model": "authentik_core.User"}]}'
) )
) )
self.assertFalse(importer.validate()) self.assertFalse(importer.validate()[0])
def test_export_validate_import(self): def test_export_validate_import(self):
"""Test export and validate it""" """Test export and validate it"""
@ -70,7 +70,7 @@ class TestFlowTransport(TransactionTestCase):
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = Importer(export_yaml)
self.assertTrue(importer.validate()) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) 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() count_initial = Prompt.objects.filter(field_key="username").count()
importer = Importer(STATIC_PROMPT_EXPORT) importer = Importer(STATIC_PROMPT_EXPORT)
self.assertTrue(importer.validate()) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count() count_before = Prompt.objects.filter(field_key="username").count()
@ -116,7 +116,7 @@ class TestFlowTransport(TransactionTestCase):
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = Importer(export_yaml)
self.assertTrue(importer.validate()) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
@ -160,5 +160,5 @@ class TestFlowTransport(TransactionTestCase):
importer = Importer(export_yaml) importer = Importer(export_yaml)
self.assertTrue(importer.validate()) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) 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.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from structlog.testing import capture_logs
from structlog.types import EventDict
from yaml import load from yaml import load
from authentik.blueprints.v1.common import ( from authentik.blueprints.v1.common import (
@ -198,17 +200,20 @@ class Importer:
self.logger.debug("updated model", model=model, pk=model.pk) self.logger.debug("updated model", model=model, pk=model.pk)
return True return True
def validate(self) -> bool: def validate(self) -> tuple[bool, list[EventDict]]:
"""Validate loaded flow export, ensure all models are allowed """Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors""" and serializers have no errors"""
self.logger.debug("Starting flow import validation") self.logger.debug("Starting blueprint import validation")
orig_import = deepcopy(self.__import) orig_import = deepcopy(self.__import)
if self.__import.version != 1: if self.__import.version != 1:
self.logger.warning("Invalid bundle version") self.logger.warning("Invalid bundle version")
return False return False, []
with transaction_rollback(): with (
transaction_rollback(),
capture_logs() as logs,
):
successful = self._apply_models() successful = self._apply_models()
if not successful: if not successful:
self.logger.debug("Flow validation failed") self.logger.debug("blueprint validation failed")
self.__import = orig_import 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""" """authentik core app config"""
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings from django.conf import settings
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikCoreConfig(AppConfig):
class AuthentikCoreConfig(ManagedAppConfig):
"""authentik core app config""" """authentik core app config"""
name = "authentik.core" name = "authentik.core"
label = "authentik_core" label = "authentik_core"
verbose_name = "authentik Core" verbose_name = "authentik Core"
mountpoint = "" mountpoint = ""
default = True
def ready(self): def reconcile_load_core_signals(self):
import_module("authentik.core.signals") """Load core signals"""
import_module("authentik.core.managed") self.import_module("authentik.core.signals")
def reconcile_debug_worker_hook(self):
"""Dispatch startup tasks inline when debugging"""
if settings.DEBUG: if settings.DEBUG:
from authentik.root.celery import worker_ready_hook from authentik.root.celery import worker_ready_hook
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.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction

View File

@ -1,16 +1,55 @@
"""authentik crypto app config""" """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""" """authentik crypto app config"""
name = "authentik.crypto" name = "authentik.crypto"
label = "authentik_crypto" label = "authentik_crypto"
verbose_name = "authentik Crypto" verbose_name = "authentik Crypto"
default = True
def ready(self): def reconcile_load_crypto_tasks(self):
import_module("authentik.crypto.managed") """Load crypto tasks"""
import_module("authentik.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""" """authentik events app"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge from prometheus_client import Gauge
from authentik.blueprints.manager import ManagedAppConfig
GAUGE_TASKS = Gauge( GAUGE_TASKS = Gauge(
"authentik_system_tasks", "authentik_system_tasks",
"System tasks and their status", "System tasks and their status",
@ -11,12 +10,14 @@ GAUGE_TASKS = Gauge(
) )
class AuthentikEventsConfig(AppConfig): class AuthentikEventsConfig(ManagedAppConfig):
"""authentik events app""" """authentik events app"""
name = "authentik.events" name = "authentik.events"
label = "authentik_events" label = "authentik_events"
verbose_name = "authentik Events" verbose_name = "authentik Events"
default = True
def ready(self): def reconcile_load_events_signals(self):
import_module("authentik.events.signals") """Load events signals"""
self.import_module("authentik.events.signals")

View File

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

View File

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

View File

@ -62,7 +62,6 @@ ldap:
tls: tls:
ciphers: null ciphers: null
config_file_dir: "/config"
cookie_domain: null cookie_domain: null
disable_update_check: false disable_update_check: false
disable_startup_analytics: false disable_startup_analytics: false
@ -79,3 +78,6 @@ gdpr_compliance: true
cert_discovery_dir: /certs cert_discovery_dir: /certs
default_token_length: 128 default_token_length: 128
impersonation: true 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.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer 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.outposts.models import Outpost, OutpostConfig, OutpostType, default_outpost_config
from authentik.providers.ldap.models import LDAPProvider from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyProvider

View File

@ -1,10 +1,9 @@
"""authentik outposts app config""" """authentik outposts app config"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge from prometheus_client import Gauge
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig
LOGGER = get_logger() LOGGER = get_logger()
GAUGE_OUTPOSTS_CONNECTED = Gauge( GAUGE_OUTPOSTS_CONNECTED = Gauge(
@ -15,15 +14,47 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
"Last update from any outpost", "Last update from any outpost",
["outpost", "uid", "version"], ["outpost", "uid", "version"],
) )
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
class AuthentikOutpostConfig(AppConfig): class AuthentikOutpostConfig(ManagedAppConfig):
"""authentik outposts app config""" """authentik outposts app config"""
name = "authentik.outposts" name = "authentik.outposts"
label = "authentik_outposts" label = "authentik_outposts"
verbose_name = "authentik Outpost" verbose_name = "authentik Outpost"
default = True
def ready(self): def reconcile_load_outposts_signals(self):
import_module("authentik.outposts.signals") """Load outposts signals"""
import_module("authentik.outposts.managed") 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 yaml import safe_dump
from authentik import __version__ from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
from authentik.outposts.docker_tls import DockerInlineTLS from authentik.outposts.docker_tls import DockerInlineTLS
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.models import ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
Outpost, Outpost,

View File

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

View File

@ -78,7 +78,7 @@ class DockerInlineSSH:
"""Cleanup when we're done""" """Cleanup when we're done"""
try: try:
os.unlink(self.key_path) 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 start = 0
end = 0 end = 0
lines = ssh_config.readlines() 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(): def outpost_local_connection():
"""Checks the local environment and create Service connections.""" """Checks the local environment and create Service connections."""
if not CONFIG.y_bool("outposts.discover"): if not CONFIG.y_bool("outposts.discover"):
LOGGER.debug("outpost integration discovery is disabled") LOGGER.debug("Outpost integration discovery is disabled")
return return
# Explicitly check against token filename, as that's # Explicitly check against token filename, as that's
# only present when the integration is enabled # only present when the integration is enabled

View File

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

View File

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

View File

@ -1,16 +1,19 @@
"""Authentik reputation_policy app config""" """Authentik reputation_policy app config"""
from importlib import import_module from authentik.blueprints.manager import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyReputationConfig(AppConfig): class AuthentikPolicyReputationConfig(ManagedAppConfig):
"""Authentik reputation app config""" """Authentik reputation app config"""
name = "authentik.policies.reputation" name = "authentik.policies.reputation"
label = "authentik_policies_reputation" label = "authentik_policies_reputation"
verbose_name = "authentik Policies.Reputation" verbose_name = "authentik Policies.Reputation"
default = True
def ready(self): def reconcile_load_policies_reputation_signals(self):
import_module("authentik.policies.reputation.signals") """Load policies.reputation signals"""
import_module("authentik.policies.reputation.tasks") 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""" """authentik oauth provider app config"""
from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
@ -14,6 +12,3 @@ class AuthentikProviderOAuth2Config(AppConfig):
"authentik.providers.oauth2.urls_github": "", "authentik.providers.oauth2.urls_github": "",
"authentik.providers.oauth2.urls": "application/o/", "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 django.urls import reverse
from jwt import decode 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.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.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
@ -24,9 +24,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenClientCredentials(OAuthTestCase): class TestTokenClientCredentials(OAuthTestCase):
"""Test token (client_credentials) view""" """Test token (client_credentials) view"""
@apply_blueprint("blueprints/system/providers-oauth2.yaml")
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
ObjectManager().run()
self.factory = RequestFactory() self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create( self.provider = OAuth2Provider.objects.create(
name="test", name="test",

View File

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

View File

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

View File

@ -1,6 +1,4 @@
"""authentik Proxy app""" """authentik Proxy app"""
from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
@ -10,6 +8,3 @@ class AuthentikProviderProxyConfig(AppConfig):
name = "authentik.providers.proxy" name = "authentik.providers.proxy"
label = "authentik_providers_proxy" label = "authentik_providers_proxy"
verbose_name = "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""" """authentik SAML IdP app config"""
from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
@ -11,6 +10,3 @@ class AuthentikProviderSAMLConfig(AppConfig):
label = "authentik_providers_saml" label = "authentik_providers_saml"
verbose_name = "authentik Providers.SAML" verbose_name = "authentik Providers.SAML"
mountpoint = "application/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.http.request import QueryDict
from django.test import RequestFactory, TestCase 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.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -74,8 +74,8 @@ qNAZMq1DqpibfCBg
class TestAuthNRequest(TestCase): class TestAuthNRequest(TestCase):
"""Test AuthN Request generator and parser""" """Test AuthN Request generator and parser"""
@apply_blueprint("blueprints/system/providers-saml.yaml")
def setUp(self): def setUp(self):
ObjectManager().run()
cert = create_test_cert() cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create( self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),

View File

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

View File

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

View File

@ -1,16 +1,15 @@
"""authentik ldap source config""" """authentik ldap source config"""
from importlib import import_module from authentik.blueprints.manager import ManagedAppConfig
from django.apps import AppConfig
class AuthentikSourceLDAPConfig(AppConfig): class AuthentikSourceLDAPConfig(ManagedAppConfig):
"""Authentik ldap app config""" """Authentik ldap app config"""
name = "authentik.sources.ldap" name = "authentik.sources.ldap"
label = "authentik_sources_ldap" label = "authentik_sources_ldap"
verbose_name = "authentik Sources.LDAP" verbose_name = "authentik Sources.LDAP"
default = True
def ready(self): def reconcile_load_sources_ldap_signals(self):
import_module("authentik.sources.ldap.signals") """Load sources.ldap signals"""
import_module("authentik.sources.ldap.managed") 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.db.models import Q
from django.test import TestCase 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.core.models import User
from authentik.lib.generators import generate_key from authentik.lib.generators import generate_key
from authentik.sources.ldap.auth import LDAPBackend from authentik.sources.ldap.auth import LDAPBackend
@ -19,8 +19,8 @@ LDAP_PASSWORD = generate_key()
class LDAPSyncTests(TestCase): class LDAPSyncTests(TestCase):
"""LDAP Sync tests""" """LDAP Sync tests"""
@apply_blueprint("blueprints/system/sources-ldap.yaml")
def setUp(self): def setUp(self):
ObjectManager().run()
self.source = LDAPSource.objects.create( self.source = LDAPSource.objects.create(
name="ldap", name="ldap",
slug="ldap", slug="ldap",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ entries:
expression: | expression: |
# Check if we''ve not been given a username by the external IdP # Check if we''ve not been given a username by the external IdP
# and trigger the enrollment flow # 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 meta_model_name: authentik_policies_expression.expressionpolicy
identifiers: identifiers:
name: default-source-enrollment-if-username name: default-source-enrollment-if-username
@ -78,7 +78,7 @@ entries:
order: 0 order: 0
stage: !KeyOf default-source-enrollment-prompt stage: !KeyOf default-source-enrollment-prompt
target: !KeyOf flow target: !KeyOf flow
id: prompt-binding id: prompt-binding
model: authentik_flows.flowstagebinding model: authentik_flows.flowstagebinding
- attrs: - attrs:
evaluate_on_plan: true evaluate_on_plan: true

View File

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

View File

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

View File

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

View File

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

View File

@ -93,7 +93,7 @@ entries:
session_duration: seconds=0 session_duration: seconds=0
- identifiers: - identifiers:
name: Change your password name: Change your password
name: stages-prompt-password id: stages-prompt-password
model: authentik_stages_prompt.promptstage model: authentik_stages_prompt.promptstage
attrs: attrs:
fields: 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 type: object
description: Info about a single blueprint instance file description: Info about a single blueprint instance file
properties: properties:
pk:
type: string
format: uuid
readOnly: true
title: Instance uuid
name: name:
type: string type: string
path: path:
@ -20877,15 +20882,26 @@ components:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
last_applied_hash:
type: string
readOnly: true
status: status:
$ref: '#/components/schemas/BlueprintInstanceStatusEnum' $ref: '#/components/schemas/BlueprintInstanceStatusEnum'
enabled: enabled:
type: boolean type: boolean
managed_models:
type: array
items:
type: string
readOnly: true
required: required:
- context - context
- last_applied - last_applied
- last_applied_hash
- managed_models
- name - name
- path - path
- pk
- status - status
BlueprintInstanceRequest: BlueprintInstanceRequest:
type: object type: object
@ -20914,6 +20930,7 @@ components:
- successful - successful
- warning - warning
- error - error
- orphaned
- unknown - unknown
type: string type: string
Cache: 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 import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait 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_static.models import AuthenticatorStaticStage
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage from tests.e2e.utils import SeleniumTestCase, retry
from tests.e2e.utils import SeleniumTestCase, apply_migration, retry
@skipUnless(platform.startswith("linux"), "requires local docker") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -25,18 +25,16 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test flow with otp stages""" """test flow with otp stages"""
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_totp_validate(self): def test_totp_validate(self):
"""test flow with otp stages""" """test flow with otp stages"""
sleep(1)
# Setup TOTP Device # Setup TOTP Device
device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6) device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6)
flow: Flow = Flow.objects.get(slug="default-authentication-flow") 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.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login() self.login()
@ -47,16 +45,17 @@ class TestFlowsAuthenticator(SeleniumTestCase):
flow_executor = self.get_shadow_root("ak-flow-executor") flow_executor = self.get_shadow_root("ak-flow-executor")
validation_stage = self.get_shadow_root("ak-stage-authenticator-validate", 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 = 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(totp.token())
code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER) code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER)
self.wait_for_url(self.if_user_url("/library")) self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user) self.assert_user(self.user)
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_stages_authenticator_totp", "0006_default_setup_flow") "blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint("blueprints/default/20-flow-default-authenticator-totp-setup.yaml")
def test_totp_setup(self): def test_totp_setup(self):
"""test TOTP Setup stage""" """test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") 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()) self.assertTrue(TOTPDevice.objects.filter(user=self.user, confirmed=True).exists())
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_stages_authenticator_static", "0005_default_setup_flow") "blueprints/default/10-flow-default-invalidation-flow.yaml",
)
@apply_blueprint("blueprints/default/20-flow-default-authenticator-static-setup.yaml")
def test_static_setup(self): def test_static_setup(self):
"""test Static OTP Setup stage""" """test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") 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 import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from authentik.blueprints import apply_blueprint
from authentik.core.models import User from authentik.core.models import User
from authentik.core.tests.utils import create_test_flow from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding 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.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_login.models import UserLoginStage
from authentik.stages.user_write.models import UserWriteStage 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -39,8 +40,10 @@ class TestFlowsEnroll(SeleniumTestCase):
} }
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_enroll_2_step(self): def test_enroll_2_step(self):
"""Test 2-step enroll flow""" """Test 2-step enroll flow"""
# First stage fields # First stage fields
@ -103,8 +106,10 @@ class TestFlowsEnroll(SeleniumTestCase):
self.assertEqual(user.email, "foo@bar.baz") self.assertEqual(user.email, "foo@bar.baz")
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_enroll_email(self): def test_enroll_email(self):
"""Test enroll with Email verification""" """Test enroll with Email verification"""
# First stage fields # First stage fields

View File

@ -2,7 +2,8 @@
from sys import platform from sys import platform
from unittest.case import skipUnless 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -10,8 +11,10 @@ class TestFlowsLogin(SeleniumTestCase):
"""test default login flow""" """test default login flow"""
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_login(self): def test_login(self):
"""test default login flow""" """test default login flow"""
self.driver.get( 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.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from authentik.blueprints import apply_blueprint
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.generators import generate_key from authentik.lib.generators import generate_key
from authentik.stages.password.models import PasswordStage 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -17,9 +18,11 @@ class TestFlowsStageSetup(SeleniumTestCase):
"""test stage setup flows""" """test stage setup flows"""
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint("blueprints/default/0-flow-password-change.yaml")
@apply_migration("authentik_flows", "0011_flow_title") @apply_blueprint(
@apply_migration("authentik_stages_password", "0002_passwordstage_change_flow") "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_password_change(self): def test_password_change(self):
"""test password change flow""" """test password change flow"""
# Ensure that password stage has change_flow set # 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 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPInvalidCredentialsResult from ldap3.core.exceptions import LDAPInvalidCredentialsResult
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow 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.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -81,8 +82,10 @@ class TestProviderLDAP(SeleniumTestCase):
return outpost return outpost
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@object_manager "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_success(self): def test_ldap_bind_success(self):
"""Test simple bind""" """Test simple bind"""
self._prepare() self._prepare()
@ -106,8 +109,10 @@ class TestProviderLDAP(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@object_manager "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_success_ssl(self): def test_ldap_bind_success_ssl(self):
"""Test simple bind with ssl""" """Test simple bind with ssl"""
self._prepare() self._prepare()
@ -131,8 +136,10 @@ class TestProviderLDAP(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@object_manager "blueprints/default/10-flow-default-authentication-flow.yaml",
"blueprints/default/10-flow-default-invalidation-flow.yaml",
)
def test_ldap_bind_fail(self): def test_ldap_bind_fail(self):
"""Test simple bind (failed)""" """Test simple bind (failed)"""
self._prepare() self._prepare()
@ -154,8 +161,11 @@ class TestProviderLDAP(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@object_manager "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): def test_ldap_bind_search(self):
"""Test simple bind + search""" """Test simple bind + search"""
outpost = self._prepare() outpost = self._prepare()

View File

@ -8,13 +8,14 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application from authentik.core.models import Application
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -56,10 +57,18 @@ class TestProviderOAuth2Github(SeleniumTestCase):
} }
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@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): def test_authorization_consent_implied(self):
"""test OAuth Provider flow (default authorization flow with implied consent)""" """test OAuth Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -104,10 +113,18 @@ class TestProviderOAuth2Github(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@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): def test_authorization_consent_explicit(self):
"""test OAuth Provider flow (default authorization flow with explicit consent)""" """test OAuth Provider flow (default authorization flow with explicit consent)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -171,10 +188,15 @@ class TestProviderOAuth2Github(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@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): def test_denied(self):
"""test OAuth Provider flow (default authorization flow, denied)""" """test OAuth Provider flow (default authorization flow, denied)"""
# Bootstrap all needed objects # 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.common.by import By
from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow from authentik.flows.models import Flow
@ -20,7 +21,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OPENID_PROFILE, SCOPE_OPENID_PROFILE,
) )
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -65,10 +66,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
} }
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@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): def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)""" """test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1) sleep(1)
@ -106,11 +115,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)""" """test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1) sleep(1)
@ -161,11 +177,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_authorization_logout(self):
"""test OpenID Provider flow with logout""" """test OpenID Provider flow with logout"""
sleep(1) sleep(1)
@ -225,11 +248,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.ID, "logout").click() self.driver.find_element(By.ID, "logout").click()
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)""" """test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1) sleep(1)
@ -298,10 +328,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@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): def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)""" """test OpenID Provider flow (default authorization with access deny)"""
sleep(1) sleep(1)

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support import expected_conditions as ec
from authentik.blueprints import apply_blueprint
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert from authentik.core.tests.utils import create_test_cert
from authentik.flows.models import Flow 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.policies.models import PolicyBinding
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
from authentik.sources.saml.processors.constants import SAML_BINDING_POST 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") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -63,11 +64,18 @@ class TestProviderSAML(SeleniumTestCase):
sleep(1) sleep(1)
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_sp_initiated_implicit(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)""" """test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -125,11 +133,18 @@ class TestProviderSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_sp_initiated_explicit(self):
"""test SAML Provider flow SP-initiated flow (explicit consent)""" """test SAML Provider flow SP-initiated flow (explicit consent)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -202,11 +217,18 @@ class TestProviderSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_sp_initiated_explicit_post(self):
"""test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -279,11 +301,18 @@ class TestProviderSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_idp_initiated_implicit(self):
"""test SAML Provider flow IdP-initiated flow (implicit consent)""" """test SAML Provider flow IdP-initiated flow (implicit consent)"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -347,11 +376,18 @@ class TestProviderSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0010_provider_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)""" """test SAML Provider flow SP-initiated flow (Policy denies access)"""
# Bootstrap all needed objects # 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 selenium.webdriver.support.wait import WebDriverWait
from yaml import safe_dump from yaml import safe_dump
from authentik.blueprints import apply_blueprint
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key 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.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.stages.identification.models import IdentificationStage 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 CONFIG_PATH = "/tmp/dex.yml" # nosec
@ -141,11 +142,19 @@ class TestSourceOAuth2(SeleniumTestCase):
ident_stage.save() ident_stage.save()
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_oauth_enroll(self):
"""test OAuth Source With With OIDC""" """test OAuth Source With With OIDC"""
self.create_objects() self.create_objects()
@ -190,11 +199,14 @@ class TestSourceOAuth2(SeleniumTestCase):
self.assert_user(User(username="foo", name="admin", email="admin@example.com")) self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_oauth_enroll_auth(self):
"""test OAuth Source With With OIDC (enroll and authenticate again)""" """test OAuth Source With With OIDC (enroll and authenticate again)"""
self.test_oauth_enroll() self.test_oauth_enroll()
@ -279,11 +291,15 @@ class TestSourceOAuth1(SeleniumTestCase):
ident_stage.save() ident_stage.save()
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@object_manager @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): def test_oauth_enroll(self):
"""test OAuth Source With With OIDC""" """test OAuth Source With With OIDC"""
self.create_objects() 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 import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from authentik.blueprints import apply_blueprint
from authentik.core.models import User from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
from authentik.stages.identification.models import IdentificationStage 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----- IDP_CERT = """-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
@ -94,12 +95,15 @@ class TestSourceSAML(SeleniumTestCase):
} }
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") @apply_blueprint(
@object_manager "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): def test_idp_redirect(self):
"""test SAML Source With redirect binding""" """test SAML Source With redirect binding"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -161,12 +165,15 @@ class TestSourceSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") @apply_blueprint(
@object_manager "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): def test_idp_post(self):
"""test SAML Source With post binding""" """test SAML Source With post binding"""
# Bootstrap all needed objects # Bootstrap all needed objects
@ -241,12 +248,15 @@ class TestSourceSAML(SeleniumTestCase):
) )
@retry() @retry()
@apply_migration("authentik_flows", "0008_default_flows") @apply_blueprint(
@apply_migration("authentik_flows", "0011_flow_title") "blueprints/default/10-flow-default-authentication-flow.yaml",
@apply_migration("authentik_flows", "0009_source_flows") "blueprints/default/10-flow-default-invalidation-flow.yaml",
@apply_migration("authentik_crypto", "0002_create_self_signed_kp") )
@apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") @apply_blueprint(
@object_manager "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): def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)""" """test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects # Bootstrap all needed objects

View File

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

View File

@ -13,7 +13,7 @@ title: Full development environment
## Services Setup ## Services Setup
For PostgreSQL and Redis, you can use the docker-compose file in `scripts/`. For PostgreSQL and Redis, you can use the docker-compose file in `scripts/`.
You can also use a native install, if you prefer. You can also use a native install, if you prefer.
## Backend Setup ## Backend Setup
@ -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 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 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`.
```yaml
debug: true
postgresql:
user: postgres
log_level: debug
secret_key: "A long key you can generate with `pwgen 40 1` for example"
```
To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream. To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream.
@ -50,7 +41,7 @@ By default, no compiled bundle of the frontend is included so this step is requi
To build the UI once, run `web-build`. To build the UI once, run `web-build`.
Alternatively, if you want to live-edit the UI, you can run `make web-watch` instead. Alternatively, if you want to live-edit the UI, you can run `make web-watch` instead.
This will immediately update the UI with any changes you make so you can see the results in real time without needing to rebuild. This will immediately update the UI with any changes you make so you can see the results in real time without needing to rebuild.
To format the frontend code, run `make web`. To format the frontend code, run `make web`.