From a023eee9bfd5453b8b75f9e2c7e7b1ced12558a1 Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 1 Aug 2022 23:05:58 +0200 Subject: [PATCH] blueprints: migrate from managed (#3338) * test all bundled blueprints Signed-off-by: Jens Langhammer * fix empty title Signed-off-by: Jens Langhammer * fix default blueprints Signed-off-by: Jens Langhammer * add script to generate dev config Signed-off-by: Jens Langhammer * migrate managed to blueprints Signed-off-by: Jens Langhammer * add more to blueprint instance Signed-off-by: Jens Langhammer * migrated away from ObjectManager Signed-off-by: Jens Langhammer * fix lint errors Signed-off-by: Jens Langhammer * migrate things Signed-off-by: Jens Langhammer * migrate tests Signed-off-by: Jens Langhammer * fix some tests Signed-off-by: Jens Langhammer * fix a bit more Signed-off-by: Jens Langhammer * fix more tests Signed-off-by: Jens Langhammer * whops Signed-off-by: Jens Langhammer * fix missing name Signed-off-by: Jens Langhammer * *sigh* Signed-off-by: Jens Langhammer * fix more tests Signed-off-by: Jens Langhammer * add tasks Signed-off-by: Jens Langhammer * scheduled Signed-off-by: Jens Langhammer * run discovery on start Signed-off-by: Jens Langhammer * oops this test should stay Signed-off-by: Jens Langhammer --- Dockerfile | 5 +- Makefile | 7 +- authentik/admin/api/system.py | 2 +- authentik/admin/apps.py | 13 +-- authentik/admin/tests/test_api.py | 5 +- authentik/api/authentication.py | 2 +- authentik/api/tests/test_auth.py | 4 +- authentik/blueprints/__init__.py | 23 +++++ authentik/blueprints/api.py | 8 ++ authentik/blueprints/apps.py | 17 ++-- .../management/commands/apply_blueprint.py | 4 +- authentik/blueprints/manager.py | 87 ++++++------------ .../blueprints/migrations/0001_initial.py | 28 +++++- authentik/blueprints/models.py | 2 + authentik/blueprints/settings.py | 13 +-- authentik/blueprints/tasks.py | 29 ------ authentik/blueprints/tests/test_bundled.py | 30 +++++++ authentik/blueprints/tests/test_managed.py | 13 --- authentik/blueprints/tests/test_transport.py | 12 +-- .../blueprints/tests/test_transport_docs.py | 29 ------ authentik/blueprints/v1/importer.py | 19 ++-- authentik/blueprints/v1/tasks.py | 84 ++++++++++++++++++ authentik/core/apps.py | 29 ++++-- authentik/core/managed.py | 17 ---- authentik/crypto/api.py | 2 +- authentik/crypto/apps.py | 51 +++++++++-- authentik/crypto/managed.py | 40 --------- authentik/events/apps.py | 13 +-- authentik/flows/api/flows.py | 3 +- authentik/flows/apps.py | 25 +++--- authentik/lib/default.yml | 4 +- authentik/outposts/api/outposts.py | 2 +- authentik/outposts/apps.py | 45 ++++++++-- authentik/outposts/controllers/docker.py | 2 +- authentik/outposts/controllers/k8s/base.py | 2 +- authentik/outposts/docker_ssh.py | 2 +- authentik/outposts/managed.py | 41 --------- authentik/outposts/tasks.py | 2 +- .../outposts/tests/test_controller_docker.py | 6 +- authentik/policies/apps.py | 13 +-- authentik/policies/reputation/apps.py | 17 ++-- authentik/providers/oauth2/apps.py | 5 -- authentik/providers/oauth2/managed.py | 60 ------------- .../providers/oauth2/tests/test_token_cc.py | 4 +- .../oauth2/tests/test_token_cc_jwt_source.py | 4 +- .../providers/oauth2/tests/test_userinfo.py | 4 +- authentik/providers/proxy/apps.py | 5 -- authentik/providers/proxy/managed.py | 29 ------ authentik/providers/saml/apps.py | 4 - authentik/providers/saml/managed.py | 74 ---------------- .../saml/tests/test_auth_n_request.py | 4 +- authentik/providers/saml/tests/test_schema.py | 4 +- authentik/root/celery.py | 4 +- authentik/sources/ldap/apps.py | 13 ++- authentik/sources/ldap/managed.py | 69 --------------- authentik/sources/ldap/tests/test_auth.py | 4 +- authentik/sources/ldap/tests/test_sync.py | 4 +- authentik/sources/oauth/apps.py | 12 +-- authentik/sources/saml/apps.py | 13 ++- authentik/stages/authenticator_static/apps.py | 12 +-- authentik/stages/email/apps.py | 20 ++--- .../20-flow-default-source-enrollment.yaml | 4 +- ...low-default-source-pre-authentication.yaml | 2 +- .../30-flow-default-user-settings-flow.yaml | 6 +- .../example/flows-enrollment-2-stage.yaml | 1 - blueprints/example/flows-login-2fa.yaml | 1 - .../flows-recovery-email-verification.yaml | 2 +- blueprints/system/providers-oauth2.yaml | 44 ++++++++++ blueprints/system/providers-proxy.yaml | 17 ++++ blueprints/system/providers-saml.yaml | 59 +++++++++++++ blueprints/system/sources-ldap.yaml | 68 ++++++++++++++ schema.yml | 17 ++++ scripts/generate_config.py | 26 ++++++ tests/e2e/test_flows_authenticators.py | 33 +++---- tests/e2e/test_flows_enroll.py | 15 ++-- tests/e2e/test_flows_login.py | 9 +- tests/e2e/test_flows_stage_setup.py | 11 ++- tests/e2e/test_provider_ldap.py | 30 ++++--- tests/e2e/test_provider_oauth2_github.py | 48 +++++++--- tests/e2e/test_provider_oauth2_grafana.py | 86 +++++++++++++----- tests/e2e/test_provider_oauth2_oidc.py | 59 +++++++++---- .../e2e/test_provider_oauth2_oidc_implicit.py | 59 +++++++++---- tests/e2e/test_provider_proxy.py | 35 +++++--- tests/e2e/test_provider_saml.py | 88 +++++++++++++------ tests/e2e/test_source_oauth.py | 48 ++++++---- tests/e2e/test_source_saml.py | 48 ++++++---- tests/e2e/utils.py | 34 ++----- .../setup/full-dev-environment.md | 15 +--- 88 files changed, 1094 insertions(+), 871 deletions(-) delete mode 100644 authentik/blueprints/tasks.py create mode 100644 authentik/blueprints/tests/test_bundled.py delete mode 100644 authentik/blueprints/tests/test_managed.py delete mode 100644 authentik/blueprints/tests/test_transport_docs.py create mode 100644 authentik/blueprints/v1/tasks.py delete mode 100644 authentik/core/managed.py delete mode 100644 authentik/crypto/managed.py delete mode 100644 authentik/outposts/managed.py delete mode 100644 authentik/providers/oauth2/managed.py delete mode 100644 authentik/providers/proxy/managed.py delete mode 100644 authentik/providers/saml/managed.py delete mode 100644 authentik/sources/ldap/managed.py create mode 100644 blueprints/system/providers-oauth2.yaml create mode 100644 blueprints/system/providers-proxy.yaml create mode 100644 blueprints/system/providers-saml.yaml create mode 100644 blueprints/system/sources-ldap.yaml create mode 100644 scripts/generate_config.py diff --git a/Dockerfile b/Dockerfile index 8859719fa..b703e90bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ RUN apt-get update && \ apt-get clean && \ rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \ adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \ - mkdir -p /certs /media && \ + mkdir -p /certs /media /blueprints && \ mkdir -p /authentik/.ssh && \ chown authentik:authentik /certs /media /authentik/.ssh @@ -82,7 +82,8 @@ COPY ./pyproject.toml / COPY ./xml /xml COPY ./tests /tests COPY ./manage.py / -COPY ./blueprints/default /blueprints +COPY ./blueprints/default /blueprints/default +COPY ./blueprints/system /blueprints/system COPY ./lifecycle/ /lifecycle COPY --from=builder /work/authentik /authentik-proxy COPY --from=web-builder /work/web/dist/ /web/dist/ diff --git a/Makefile b/Makefile index 820bb0339..f00b68f08 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,8 @@ test: coverage report lint-fix: - isort authentik tests lifecycle - black authentik tests lifecycle + isort authentik tests scripts lifecycle + black authentik tests scripts lifecycle codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \ authentik \ internal \ @@ -91,6 +91,9 @@ gen-client-go: go mod edit -replace goauthentik.io/api/v3=./gen-go-api rm -rf config.yaml ./templates/ +gen-dev-config: + python -m scripts.generate_config + gen: gen-build gen-clean gen-client-web migrate: diff --git a/authentik/admin/api/system.py b/authentik/admin/api/system.py index 43cb00b1b..18c379bc5 100644 --- a/authentik/admin/api/system.py +++ b/authentik/admin/api/system.py @@ -16,7 +16,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from authentik.core.api.utils import PassiveSerializer -from authentik.outposts.managed import MANAGED_OUTPOST +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.models import Outpost diff --git a/authentik/admin/apps.py b/authentik/admin/apps.py index fc3d74fee..ea1818622 100644 --- a/authentik/admin/apps.py +++ b/authentik/admin/apps.py @@ -1,19 +1,20 @@ """authentik admin app config""" -from importlib import import_module - -from django.apps import AppConfig from prometheus_client import Gauge, Info +from authentik.blueprints.manager import ManagedAppConfig + PROM_INFO = Info("authentik_version", "Currently running authentik version") GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") -class AuthentikAdminConfig(AppConfig): +class AuthentikAdminConfig(ManagedAppConfig): """authentik admin app config""" name = "authentik.admin" label = "authentik_admin" verbose_name = "authentik Admin" + default = True - def ready(self): - import_module("authentik.admin.signals") + def reconcile_load_admin_signals(self): + """Load admin signals""" + self.import_module("authentik.admin.signals") diff --git a/authentik/admin/tests/test_api.py b/authentik/admin/tests/test_api.py index 5ef8ff29c..cc42ae022 100644 --- a/authentik/admin/tests/test_api.py +++ b/authentik/admin/tests/test_api.py @@ -1,11 +1,11 @@ """test admin api""" from json import loads +from django.apps import apps from django.test import TestCase from django.urls import reverse from authentik import __version__ -from authentik.blueprints.tasks import managed_reconcile from authentik.core.models import Group, User from authentik.core.tasks import clean_expired_models from authentik.events.monitored_tasks import TaskResultStatus @@ -95,7 +95,6 @@ class TestAdminAPI(TestCase): def test_system(self): """Test system API""" - # pyright: reportGeneralTypeIssues=false - managed_reconcile() # pylint: disable=no-value-for-parameter + apps.get_app_config("authentik_outposts").reconcile_embedded_outpost() response = self.client.get(reverse("authentik_api:admin_system")) self.assertEqual(response.status_code, 200) diff --git a/authentik/api/authentication.py b/authentik/api/authentication.py index 4f3e65909..501001605 100644 --- a/authentik/api/authentication.py +++ b/authentik/api/authentication.py @@ -65,7 +65,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]: def token_secret_key(value: str) -> Optional[User]: """Check if the token is the secret key and return the service account for the managed outpost""" - from authentik.outposts.managed import MANAGED_OUTPOST + from authentik.outposts.apps import MANAGED_OUTPOST if value != settings.SECRET_KEY: return None diff --git a/authentik/api/tests/test_auth.py b/authentik/api/tests/test_auth.py index 91b05b3cf..73f76c1a4 100644 --- a/authentik/api/tests/test_auth.py +++ b/authentik/api/tests/test_auth.py @@ -1,6 +1,7 @@ """Test API Authentication""" from base64 import b64encode +from django.apps import apps from django.conf import settings from django.test import TestCase from guardian.shortcuts import get_anonymous_user @@ -10,7 +11,6 @@ from authentik.api.authentication import bearer_auth from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents from authentik.core.tests.utils import create_test_flow from authentik.lib.generators import generate_id -from authentik.outposts.managed import OutpostManager from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken @@ -44,7 +44,7 @@ class TestAPIAuth(TestCase): with self.assertRaises(AuthenticationFailed): user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) - OutpostManager().run() + apps.get_app_config("authentik_outposts").reconcile_embedded_outpost() user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True) diff --git a/authentik/blueprints/__init__.py b/authentik/blueprints/__init__.py index e69de29bb..ab442d844 100644 --- a/authentik/blueprints/__init__.py +++ b/authentik/blueprints/__init__.py @@ -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 diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index 3fe8e5ecc..bcfe9f311 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -27,13 +27,21 @@ class BlueprintInstanceSerializer(ModelSerializer): model = BlueprintInstance fields = [ + "pk", "name", "path", "context", "last_applied", + "last_applied_hash", "status", "enabled", + "managed_models", ] + extra_kwargs = { + "last_applied": {"read_only": True}, + "last_applied_hash": {"read_only": True}, + "managed_models": {"read_only": True}, + } class BlueprintInstanceViewSet(ModelViewSet): diff --git a/authentik/blueprints/apps.py b/authentik/blueprints/apps.py index e413a4eac..1ce185089 100644 --- a/authentik/blueprints/apps.py +++ b/authentik/blueprints/apps.py @@ -1,15 +1,22 @@ """authentik Blueprints app""" -from django.apps import AppConfig + +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikBlueprintsConfig(AppConfig): +class AuthentikBlueprintsConfig(ManagedAppConfig): """authentik Blueprints app""" name = "authentik.blueprints" label = "authentik_blueprints" verbose_name = "authentik Blueprints" + default = True - def ready(self) -> None: - from authentik.blueprints.tasks import managed_reconcile + def reconcile_load_blueprints_v1_tasks(self): + """Load v1 tasks""" + self.import_module("authentik.blueprints.v1.tasks") - managed_reconcile.delay() + def reconcile_blueprints_discover(self): + """Run blueprint discovery""" + from authentik.blueprints.v1.tasks import blueprints_discover + + blueprints_discover.delay() diff --git a/authentik/blueprints/management/commands/apply_blueprint.py b/authentik/blueprints/management/commands/apply_blueprint.py index 303ffcfcf..9f75a5482 100644 --- a/authentik/blueprints/management/commands/apply_blueprint.py +++ b/authentik/blueprints/management/commands/apply_blueprint.py @@ -13,9 +13,9 @@ class Command(BaseCommand): # pragma: no cover for blueprint_path in options.get("blueprints", []): with open(blueprint_path, "r", encoding="utf8") as blueprint_file: importer = Importer(blueprint_file.read()) - valid = importer.validate() + valid, logs = importer.validate() if not valid: - raise ValueError("blueprint invalid") + raise ValueError(f"blueprint invalid: {logs}") importer.apply() def add_arguments(self, parser): diff --git a/authentik/blueprints/manager.py b/authentik/blueprints/manager.py index e5807b515..d4e98b86c 100644 --- a/authentik/blueprints/manager.py +++ b/authentik/blueprints/manager.py @@ -1,70 +1,37 @@ """Managed objects manager""" -from typing import Callable, Optional +from importlib import import_module +from inspect import ismethod +from django.apps import AppConfig +from django.db import DatabaseError, ProgrammingError from structlog.stdlib import get_logger -from authentik.blueprints.models import ManagedModel - LOGGER = get_logger() -class EnsureOp: - """Ensure operation, executed as part of an ObjectManager run""" +class ManagedAppConfig(AppConfig): + """Basic reconciliation logic for apps""" - _obj: type[ManagedModel] - _managed_uid: str - _kwargs: dict + def ready(self) -> None: + self.reconcile() + return super().ready() - def __init__(self, obj: type[ManagedModel], managed_uid: str, **kwargs) -> None: - self._obj = obj - self._managed_uid = managed_uid - self._kwargs = kwargs + def import_module(self, path: str): + """Load module""" + import_module(path) - def run(self): - """Do the actual ensure action""" - raise NotImplementedError - - -class EnsureExists(EnsureOp): - """Ensure object exists, with kwargs as given values""" - - created_callback: Optional[Callable] - - def __init__( - self, - obj: type[ManagedModel], - managed_uid: str, - created_callback: Optional[Callable] = None, - **kwargs, - ) -> None: - super().__init__(obj, managed_uid, **kwargs) - self.created_callback = created_callback - - def run(self): - self._kwargs.setdefault("managed", self._managed_uid) - obj, created = self._obj.objects.update_or_create( - **{ - "managed": self._managed_uid, - "defaults": self._kwargs, - } - ) - if created and self.created_callback is not None: - self.created_callback(obj) - - -class ObjectManager: - """Base class for Apps Object manager""" - - def run(self): - """Main entrypoint for tasks, iterate through all implementation of this - and execute all operations""" - for sub in ObjectManager.__subclasses__(): - sub_inst = sub() - ops = sub_inst.reconcile() - LOGGER.debug("Reconciling managed objects", manager=sub.__name__) - for operation in ops: - operation.run() - - def reconcile(self) -> list[EnsureOp]: - """Method which is implemented in subclass that returns a list of Operations""" - raise NotImplementedError + def reconcile(self) -> None: + """reconcile ourselves""" + prefix = "reconcile_" + for meth_name in dir(self): + meth = getattr(self, meth_name) + if not ismethod(meth): + continue + if not meth_name.startswith(prefix): + continue + name = meth_name.replace(prefix, "") + try: + meth() + LOGGER.debug("Successfully reconciled", name=name) + except (ProgrammingError, DatabaseError) as exc: + LOGGER.debug("Failed to run reconcile", name=name, exc=exc) diff --git a/authentik/blueprints/migrations/0001_initial.py b/authentik/blueprints/migrations/0001_initial.py index 5bc8ed913..d337f8364 100644 --- a/authentik/blueprints/migrations/0001_initial.py +++ b/authentik/blueprints/migrations/0001_initial.py @@ -1,16 +1,37 @@ -# Generated by Django 4.0.6 on 2022-07-30 22:45 +# Generated by Django 4.0.6 on 2022-07-31 17:35 import uuid import django.contrib.postgres.fields +from django.apps.registry import Apps from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + from authentik.blueprints.v1.tasks import blueprints_discover + + BlueprintInstance = apps.get_model("authentik_blueprints", "BlueprintInstance") + Flow = apps.get_model("authentik_flows", "Flow") + + db_alias = schema_editor.connection.alias + blueprints_discover() + for blueprint in BlueprintInstance.objects.using(db_alias).all(): + # If we already have flows (and we should always run before flow migrations) + # then this is an existing install and we want to disable all blueprints + if Flow.objects.using(db_alias).all().exists(): + blueprint.enabled = False + # System blueprints are always enabled + if "/system/" in blueprint.path: + blueprint.enabled = True + blueprint.save() class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [("authentik_flows", "0001_initial")] operations = [ migrations.CreateModel( @@ -38,6 +59,7 @@ class Migration(migrations.Migration): ("path", models.TextField()), ("context", models.JSONField()), ("last_applied", models.DateTimeField(auto_now=True)), + ("last_applied_hash", models.TextField()), ( "status", models.TextField( @@ -45,6 +67,7 @@ class Migration(migrations.Migration): ("successful", "Successful"), ("warning", "Warning"), ("error", "Error"), + ("orphaned", "Orphaned"), ("unknown", "Unknown"), ] ), @@ -63,4 +86,5 @@ class Migration(migrations.Migration): "unique_together": {("name", "path")}, }, ), + migrations.RunPython(migration_blueprint_import), ] diff --git a/authentik/blueprints/models.py b/authentik/blueprints/models.py index af20f0ee2..d0aba0659 100644 --- a/authentik/blueprints/models.py +++ b/authentik/blueprints/models.py @@ -38,6 +38,7 @@ class BlueprintInstanceStatus(models.TextChoices): SUCCESSFUL = "successful" WARNING = "warning" ERROR = "error" + ORPHANED = "orphaned" UNKNOWN = "unknown" @@ -51,6 +52,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): path = models.TextField() context = models.JSONField() last_applied = models.DateTimeField(auto_now=True) + last_applied_hash = models.TextField() status = models.TextField(choices=BlueprintInstanceStatus.choices) enabled = models.BooleanField(default=True) managed_models = ArrayField(models.TextField()) diff --git a/authentik/blueprints/settings.py b/authentik/blueprints/settings.py index bd8d5842e..ca484d784 100644 --- a/authentik/blueprints/settings.py +++ b/authentik/blueprints/settings.py @@ -1,17 +1,12 @@ -"""managed Settings""" +"""blueprint Settings""" from celery.schedules import crontab from authentik.lib.utils.time import fqdn_rand CELERY_BEAT_SCHEDULE = { - "blueprints_reconcile": { - "task": "authentik.blueprints.tasks.managed_reconcile", - "schedule": crontab(minute=fqdn_rand("managed_reconcile"), hour="*/4"), - "options": {"queue": "authentik_scheduled"}, - }, - "blueprints_config_file_discovery": { - "task": "authentik.blueprints.tasks.config_file_discovery", - "schedule": crontab(minute=fqdn_rand("config_file_discovery"), hour="*"), + "blueprints_v1_discover": { + "task": "authentik.blueprints.v1.tasks.blueprints_discover", + "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"), "options": {"queue": "authentik_scheduled"}, }, } diff --git a/authentik/blueprints/tasks.py b/authentik/blueprints/tasks.py deleted file mode 100644 index c7577e3b7..000000000 --- a/authentik/blueprints/tasks.py +++ /dev/null @@ -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)])) diff --git a/authentik/blueprints/tests/test_bundled.py b/authentik/blueprints/tests/test_bundled.py new file mode 100644 index 000000000..60bd5fe3c --- /dev/null +++ b/authentik/blueprints/tests/test_bundled.py @@ -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)) diff --git a/authentik/blueprints/tests/test_managed.py b/authentik/blueprints/tests/test_managed.py deleted file mode 100644 index ccdb78949..000000000 --- a/authentik/blueprints/tests/test_managed.py +++ /dev/null @@ -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 diff --git a/authentik/blueprints/tests/test_transport.py b/authentik/blueprints/tests/test_transport.py index fded6450b..01779e5ae 100644 --- a/authentik/blueprints/tests/test_transport.py +++ b/authentik/blueprints/tests/test_transport.py @@ -37,14 +37,14 @@ class TestFlowTransport(TransactionTestCase): def test_bundle_invalid_format(self): """Test bundle with invalid format""" importer = Importer('{"version": 3}') - self.assertFalse(importer.validate()) + self.assertFalse(importer.validate()[0]) importer = Importer( ( '{"version": 1,"entries":[{"identifiers":{},"attrs":{},' '"model": "authentik_core.User"}]}' ) ) - self.assertFalse(importer.validate()) + self.assertFalse(importer.validate()[0]) def test_export_validate_import(self): """Test export and validate it""" @@ -70,7 +70,7 @@ class TestFlowTransport(TransactionTestCase): export_yaml = exporter.export_to_string() importer = Importer(export_yaml) - self.assertTrue(importer.validate()) + self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) @@ -80,7 +80,7 @@ class TestFlowTransport(TransactionTestCase): count_initial = Prompt.objects.filter(field_key="username").count() importer = Importer(STATIC_PROMPT_EXPORT) - self.assertTrue(importer.validate()) + self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) count_before = Prompt.objects.filter(field_key="username").count() @@ -116,7 +116,7 @@ class TestFlowTransport(TransactionTestCase): export_yaml = exporter.export_to_string() importer = Importer(export_yaml) - self.assertTrue(importer.validate()) + self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) @@ -160,5 +160,5 @@ class TestFlowTransport(TransactionTestCase): importer = Importer(export_yaml) - self.assertTrue(importer.validate()) + self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) diff --git a/authentik/blueprints/tests/test_transport_docs.py b/authentik/blueprints/tests/test_transport_docs.py deleted file mode 100644 index f40d90e59..000000000 --- a/authentik/blueprints/tests/test_transport_docs.py +++ /dev/null @@ -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)) diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index bbbb70fae..56d15a548 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -13,6 +13,8 @@ from django.db.utils import IntegrityError from rest_framework.exceptions import ValidationError from rest_framework.serializers import BaseSerializer, Serializer from structlog.stdlib import BoundLogger, get_logger +from structlog.testing import capture_logs +from structlog.types import EventDict from yaml import load from authentik.blueprints.v1.common import ( @@ -198,17 +200,20 @@ class Importer: self.logger.debug("updated model", model=model, pk=model.pk) return True - def validate(self) -> bool: - """Validate loaded flow export, ensure all models are allowed + def validate(self) -> tuple[bool, list[EventDict]]: + """Validate loaded blueprint export, ensure all models are allowed and serializers have no errors""" - self.logger.debug("Starting flow import validation") + self.logger.debug("Starting blueprint import validation") orig_import = deepcopy(self.__import) if self.__import.version != 1: self.logger.warning("Invalid bundle version") - return False - with transaction_rollback(): + return False, [] + with ( + transaction_rollback(), + capture_logs() as logs, + ): successful = self._apply_models() if not successful: - self.logger.debug("Flow validation failed") + self.logger.debug("blueprint validation failed") self.__import = orig_import - return successful + return successful, logs diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py new file mode 100644 index 000000000..8f81c1377 --- /dev/null +++ b/authentik/blueprints/v1/tasks.py @@ -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)) diff --git a/authentik/core/apps.py b/authentik/core/apps.py index 155cd0e46..4d456033f 100644 --- a/authentik/core/apps.py +++ b/authentik/core/apps.py @@ -1,22 +1,37 @@ """authentik core app config""" -from importlib import import_module - -from django.apps import AppConfig from django.conf import settings +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikCoreConfig(AppConfig): + +class AuthentikCoreConfig(ManagedAppConfig): """authentik core app config""" name = "authentik.core" label = "authentik_core" verbose_name = "authentik Core" mountpoint = "" + default = True - def ready(self): - import_module("authentik.core.signals") - import_module("authentik.core.managed") + def reconcile_load_core_signals(self): + """Load core signals""" + self.import_module("authentik.core.signals") + + def reconcile_debug_worker_hook(self): + """Dispatch startup tasks inline when debugging""" if settings.DEBUG: from authentik.root.celery import worker_ready_hook worker_ready_hook() + + def reconcile_source_inbuilt(self): + """Reconcile inbuilt source""" + from authentik.core.models import Source + + Source.objects.update_or_create( + defaults={ + "name": "authentik Built-in", + "slug": "authentik-built-in", + }, + managed="goauthentik.io/sources/inbuilt", + ) diff --git a/authentik/core/managed.py b/authentik/core/managed.py deleted file mode 100644 index 627a9940d..000000000 --- a/authentik/core/managed.py +++ /dev/null @@ -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", - ), - ] diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index 68fc555a7..673537a28 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -22,8 +22,8 @@ from structlog.stdlib import get_logger from authentik.api.decorators import permission_required from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer +from authentik.crypto.apps import MANAGED_KEY from authentik.crypto.builder import CertificateBuilder -from authentik.crypto.managed import MANAGED_KEY from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction diff --git a/authentik/crypto/apps.py b/authentik/crypto/apps.py index 17da6d2cc..119067c4f 100644 --- a/authentik/crypto/apps.py +++ b/authentik/crypto/apps.py @@ -1,16 +1,55 @@ """authentik crypto app config""" -from importlib import import_module +from datetime import datetime +from typing import TYPE_CHECKING, Optional -from django.apps import AppConfig +from authentik.blueprints.manager import ManagedAppConfig + +if TYPE_CHECKING: + from authentik.crypto.models import CertificateKeyPair + +MANAGED_KEY = "goauthentik.io/crypto/jwt-managed" -class AuthentikCryptoConfig(AppConfig): +class AuthentikCryptoConfig(ManagedAppConfig): """authentik crypto app config""" name = "authentik.crypto" label = "authentik_crypto" verbose_name = "authentik Crypto" + default = True - def ready(self): - import_module("authentik.crypto.managed") - import_module("authentik.crypto.tasks") + def reconcile_load_crypto_tasks(self): + """Load crypto tasks""" + self.import_module("authentik.crypto.tasks") + + def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None): + from authentik.crypto.builder import CertificateBuilder + from authentik.crypto.models import CertificateKeyPair + + builder = CertificateBuilder() + builder.common_name = "goauthentik.io" + builder.build( + subject_alt_names=["goauthentik.io"], + validity_days=360, + ) + if not cert: + + cert = CertificateKeyPair() + cert.certificate_data = builder.certificate + cert.key_data = builder.private_key + cert.name = "authentik Internal JWT Certificate" + cert.managed = MANAGED_KEY + cert.save() + + def reconcile_managed_jwt_cert(self): + """Ensure managed JWT certificate""" + from authentik.crypto.models import CertificateKeyPair + + certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY) + if not certs.exists(): + self._create_update_cert() + return + cert: CertificateKeyPair = certs.first() + now = datetime.now() + if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after: + self._create_update_cert(cert) diff --git a/authentik/crypto/managed.py b/authentik/crypto/managed.py deleted file mode 100644 index 3eb2198b6..000000000 --- a/authentik/crypto/managed.py +++ /dev/null @@ -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 [] diff --git a/authentik/events/apps.py b/authentik/events/apps.py index 814b581d7..79cf35097 100644 --- a/authentik/events/apps.py +++ b/authentik/events/apps.py @@ -1,9 +1,8 @@ """authentik events app""" -from importlib import import_module - -from django.apps import AppConfig from prometheus_client import Gauge +from authentik.blueprints.manager import ManagedAppConfig + GAUGE_TASKS = Gauge( "authentik_system_tasks", "System tasks and their status", @@ -11,12 +10,14 @@ GAUGE_TASKS = Gauge( ) -class AuthentikEventsConfig(AppConfig): +class AuthentikEventsConfig(ManagedAppConfig): """authentik events app""" name = "authentik.events" label = "authentik_events" verbose_name = "authentik Events" + default = True - def ready(self): - import_module("authentik.events.signals") + def reconcile_load_events_signals(self): + """Load events signals""" + self.import_module("authentik.events.signals") diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index c7877cb6c..467e610e7 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -168,7 +168,8 @@ class FlowViewSet(UsedByMixin, ModelViewSet): if not file: return HttpResponseBadRequest() importer = Importer(file.read().decode()) - valid = importer.validate() + valid, _logs = importer.validate() + # TODO: return logs if not valid: return HttpResponseBadRequest() successful = importer.apply() diff --git a/authentik/flows/apps.py b/authentik/flows/apps.py index 3940ec739..f02e92023 100644 --- a/authentik/flows/apps.py +++ b/authentik/flows/apps.py @@ -1,10 +1,7 @@ """authentik flows app config""" -from importlib import import_module - -from django.apps import AppConfig -from django.db.utils import ProgrammingError from prometheus_client import Gauge, Histogram +from authentik.blueprints.manager import ManagedAppConfig from authentik.lib.utils.reflection import all_subclasses GAUGE_FLOWS_CACHED = Gauge( @@ -18,20 +15,22 @@ HIST_FLOWS_PLAN_TIME = Histogram( ) -class AuthentikFlowsConfig(AppConfig): +class AuthentikFlowsConfig(ManagedAppConfig): """authentik flows app config""" name = "authentik.flows" label = "authentik_flows" mountpoint = "flows/" verbose_name = "authentik Flows" + default = True - def ready(self): - import_module("authentik.flows.signals") - try: - from authentik.flows.models import Stage + def reconcile_load_flows_signals(self): + """Load flows signals""" + self.import_module("authentik.flows.signals") - for stage in all_subclasses(Stage): - _ = stage().type - except ProgrammingError: - pass + def reconcile_stages_loaded(self): + """Ensure all stages are loaded""" + from authentik.flows.models import Stage + + for stage in all_subclasses(Stage): + _ = stage().type diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 75e1f8bb7..c65408424 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -62,7 +62,6 @@ ldap: tls: ciphers: null -config_file_dir: "/config" cookie_domain: null disable_update_check: false disable_startup_analytics: false @@ -79,3 +78,6 @@ gdpr_compliance: true cert_discovery_dir: /certs default_token_length: 128 impersonation: true + +blueprint_locations: + - /blueprints diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index 9df29d117..cc6fae88c 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer, is_dict from authentik.core.models import Provider from authentik.outposts.api.service_connections import ServiceConnectionSerializer -from authentik.outposts.managed import MANAGED_OUTPOST +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.models import Outpost, OutpostConfig, OutpostType, default_outpost_config from authentik.providers.ldap.models import LDAPProvider from authentik.providers.proxy.models import ProxyProvider diff --git a/authentik/outposts/apps.py b/authentik/outposts/apps.py index 4b29ed3fe..54ab3141d 100644 --- a/authentik/outposts/apps.py +++ b/authentik/outposts/apps.py @@ -1,10 +1,9 @@ """authentik outposts app config""" -from importlib import import_module - -from django.apps import AppConfig from prometheus_client import Gauge from structlog.stdlib import get_logger +from authentik.blueprints.manager import ManagedAppConfig + LOGGER = get_logger() GAUGE_OUTPOSTS_CONNECTED = Gauge( @@ -15,15 +14,47 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( "Last update from any outpost", ["outpost", "uid", "version"], ) +MANAGED_OUTPOST = "goauthentik.io/outposts/embedded" -class AuthentikOutpostConfig(AppConfig): +class AuthentikOutpostConfig(ManagedAppConfig): """authentik outposts app config""" name = "authentik.outposts" label = "authentik_outposts" verbose_name = "authentik Outpost" + default = True - def ready(self): - import_module("authentik.outposts.signals") - import_module("authentik.outposts.managed") + def reconcile_load_outposts_signals(self): + """Load outposts signals""" + self.import_module("authentik.outposts.signals") + + def reconcile_embedded_outpost(self): + """Ensure embedded outpost""" + from authentik.outposts.models import ( + DockerServiceConnection, + KubernetesServiceConnection, + Outpost, + OutpostConfig, + OutpostType, + ) + + outpost, updated = Outpost.objects.update_or_create( + defaults={ + "name": "authentik Embedded Outpost", + "type": OutpostType.PROXY, + }, + managed=MANAGED_OUTPOST, + ) + if updated: + if KubernetesServiceConnection.objects.exists(): + outpost.service_connection = KubernetesServiceConnection.objects.first() + elif DockerServiceConnection.objects.exists(): + outpost.service_connection = DockerServiceConnection.objects.first() + outpost.config = OutpostConfig( + kubernetes_disabled_components=[ + "deployment", + "secret", + ] + ) + outpost.save() diff --git a/authentik/outposts/controllers/docker.py b/authentik/outposts/controllers/docker.py index af4c08651..c156737b7 100644 --- a/authentik/outposts/controllers/docker.py +++ b/authentik/outposts/controllers/docker.py @@ -14,10 +14,10 @@ from structlog.stdlib import get_logger from yaml import safe_dump from authentik import __version__ +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException from authentik.outposts.docker_tls import DockerInlineTLS -from authentik.outposts.managed import MANAGED_OUTPOST from authentik.outposts.models import ( DockerServiceConnection, Outpost, diff --git a/authentik/outposts/controllers/k8s/base.py b/authentik/outposts/controllers/k8s/base.py index 44506b5d8..a1dd6a1c2 100644 --- a/authentik/outposts/controllers/k8s/base.py +++ b/authentik/outposts/controllers/k8s/base.py @@ -10,8 +10,8 @@ from structlog.stdlib import get_logger from urllib3.exceptions import HTTPError from authentik import __version__ +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate -from authentik.outposts.managed import MANAGED_OUTPOST if TYPE_CHECKING: from authentik.outposts.controllers.kubernetes import KubernetesController diff --git a/authentik/outposts/docker_ssh.py b/authentik/outposts/docker_ssh.py index 657e38a76..ff577371d 100644 --- a/authentik/outposts/docker_ssh.py +++ b/authentik/outposts/docker_ssh.py @@ -78,7 +78,7 @@ class DockerInlineSSH: """Cleanup when we're done""" try: os.unlink(self.key_path) - with open(self.config_path, "r+", encoding="utf-8") as ssh_config: + with open(self.config_path, "r", encoding="utf-8") as ssh_config: start = 0 end = 0 lines = ssh_config.readlines() diff --git a/authentik/outposts/managed.py b/authentik/outposts/managed.py deleted file mode 100644 index d8bfd5964..000000000 --- a/authentik/outposts/managed.py +++ /dev/null @@ -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, - ), - ] diff --git a/authentik/outposts/tasks.py b/authentik/outposts/tasks.py index e5a074f54..9a4c00ddc 100644 --- a/authentik/outposts/tasks.py +++ b/authentik/outposts/tasks.py @@ -233,7 +233,7 @@ def _outpost_single_update(outpost: Outpost, layer=None): def outpost_local_connection(): """Checks the local environment and create Service connections.""" if not CONFIG.y_bool("outposts.discover"): - LOGGER.debug("outpost integration discovery is disabled") + LOGGER.debug("Outpost integration discovery is disabled") return # Explicitly check against token filename, as that's # only present when the integration is enabled diff --git a/authentik/outposts/tests/test_controller_docker.py b/authentik/outposts/tests/test_controller_docker.py index 6757a34b7..d45e02b70 100644 --- a/authentik/outposts/tests/test_controller_docker.py +++ b/authentik/outposts/tests/test_controller_docker.py @@ -1,11 +1,11 @@ """Docker controller tests""" +from django.apps import apps from django.test import TestCase from docker.models.containers import Container -from authentik.blueprints.manager import ObjectManager +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.controllers.base import ControllerException from authentik.outposts.controllers.docker import DockerController -from authentik.outposts.managed import MANAGED_OUTPOST from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType from authentik.providers.proxy.controllers.docker import ProxyDockerController @@ -19,7 +19,7 @@ class DockerControllerTests(TestCase): type=OutpostType.PROXY, ) self.integration = DockerServiceConnection(name="test") - ObjectManager().run() + apps.get_app_config("authentik_outposts").reconcile() def test_init_managed(self): """Docker controller shouldn't do anything for managed outpost""" diff --git a/authentik/policies/apps.py b/authentik/policies/apps.py index fdb2f5311..025c13c54 100644 --- a/authentik/policies/apps.py +++ b/authentik/policies/apps.py @@ -1,9 +1,8 @@ """authentik policies app config""" -from importlib import import_module - -from django.apps import AppConfig from prometheus_client import Gauge, Histogram +from authentik.blueprints.manager import ManagedAppConfig + GAUGE_POLICIES_CACHED = Gauge( "authentik_policies_cached", "Cached Policies", @@ -27,12 +26,14 @@ HIST_POLICIES_EXECUTION_TIME = Histogram( ) -class AuthentikPoliciesConfig(AppConfig): +class AuthentikPoliciesConfig(ManagedAppConfig): """authentik policies app config""" name = "authentik.policies" label = "authentik_policies" verbose_name = "authentik Policies" + default = True - def ready(self): - import_module("authentik.policies.signals") + def reconcile_load_policies_signals(self): + """Load policies signals""" + self.import_module("authentik.policies.signals") diff --git a/authentik/policies/reputation/apps.py b/authentik/policies/reputation/apps.py index f54a77a0a..b92c721bf 100644 --- a/authentik/policies/reputation/apps.py +++ b/authentik/policies/reputation/apps.py @@ -1,16 +1,19 @@ """Authentik reputation_policy app config""" -from importlib import import_module - -from django.apps import AppConfig +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikPolicyReputationConfig(AppConfig): +class AuthentikPolicyReputationConfig(ManagedAppConfig): """Authentik reputation app config""" name = "authentik.policies.reputation" label = "authentik_policies_reputation" verbose_name = "authentik Policies.Reputation" + default = True - def ready(self): - import_module("authentik.policies.reputation.signals") - import_module("authentik.policies.reputation.tasks") + def reconcile_load_policies_reputation_signals(self): + """Load policies.reputation signals""" + self.import_module("authentik.policies.reputation.signals") + + def reconcile_load_policies_reputation_tasks(self): + """Load policies.reputation tasks""" + self.import_module("authentik.policies.reputation.tasks") diff --git a/authentik/providers/oauth2/apps.py b/authentik/providers/oauth2/apps.py index 8e5cc768f..437618c94 100644 --- a/authentik/providers/oauth2/apps.py +++ b/authentik/providers/oauth2/apps.py @@ -1,6 +1,4 @@ """authentik oauth provider app config""" -from importlib import import_module - from django.apps import AppConfig @@ -14,6 +12,3 @@ class AuthentikProviderOAuth2Config(AppConfig): "authentik.providers.oauth2.urls_github": "", "authentik.providers.oauth2.urls": "application/o/", } - - def ready(self) -> None: - import_module("authentik.providers.oauth2.managed") diff --git a/authentik/providers/oauth2/managed.py b/authentik/providers/oauth2/managed.py deleted file mode 100644 index 4b073ec18..000000000 --- a/authentik/providers/oauth2/managed.py +++ /dev/null @@ -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, - ), - ] diff --git a/authentik/providers/oauth2/tests/test_token_cc.py b/authentik/providers/oauth2/tests/test_token_cc.py index 212e35dc1..9707b54e0 100644 --- a/authentik/providers/oauth2/tests/test_token_cc.py +++ b/authentik/providers/oauth2/tests/test_token_cc.py @@ -5,7 +5,7 @@ from django.test import RequestFactory from django.urls import reverse from jwt import decode -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.lib.generators import generate_id, generate_key @@ -24,9 +24,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase class TestTokenClientCredentials(OAuthTestCase): """Test token (client_credentials) view""" + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def setUp(self) -> None: super().setUp() - ObjectManager().run() self.factory = RequestFactory() self.provider = OAuth2Provider.objects.create( name="test", diff --git a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py index 5e24dfba5..e41b5f0ec 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py +++ b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py @@ -6,7 +6,7 @@ from django.test import RequestFactory from django.urls import reverse from jwt import decode -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.models import Application, Group from authentik.core.tests.utils import create_test_cert, create_test_flow from authentik.lib.generators import generate_id, generate_key @@ -26,9 +26,9 @@ from authentik.sources.oauth.models import OAuthSource class TestTokenClientCredentialsJWTSource(OAuthTestCase): """Test token (client_credentials, with JWT) view""" + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def setUp(self) -> None: super().setUp() - ObjectManager().run() self.factory = RequestFactory() self.cert = create_test_cert() diff --git a/authentik/providers/oauth2/tests/test_userinfo.py b/authentik/providers/oauth2/tests/test_userinfo.py index cab69641c..93849ebfc 100644 --- a/authentik/providers/oauth2/tests/test_userinfo.py +++ b/authentik/providers/oauth2/tests/test_userinfo.py @@ -4,7 +4,7 @@ from dataclasses import asdict from django.urls import reverse -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.events.models import Event, EventAction @@ -16,9 +16,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase class TestUserinfo(OAuthTestCase): """Test token view""" + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def setUp(self) -> None: super().setUp() - ObjectManager().run() self.app = Application.objects.create(name=generate_id(), slug=generate_id()) self.provider: OAuth2Provider = OAuth2Provider.objects.create( name=generate_id(), diff --git a/authentik/providers/proxy/apps.py b/authentik/providers/proxy/apps.py index 5355ece60..ef7d2dd6d 100644 --- a/authentik/providers/proxy/apps.py +++ b/authentik/providers/proxy/apps.py @@ -1,6 +1,4 @@ """authentik Proxy app""" -from importlib import import_module - from django.apps import AppConfig @@ -10,6 +8,3 @@ class AuthentikProviderProxyConfig(AppConfig): name = "authentik.providers.proxy" label = "authentik_providers_proxy" verbose_name = "authentik Providers.Proxy" - - def ready(self) -> None: - import_module("authentik.providers.proxy.managed") diff --git a/authentik/providers/proxy/managed.py b/authentik/providers/proxy/managed.py deleted file mode 100644 index ebfbad4bd..000000000 --- a/authentik/providers/proxy/managed.py +++ /dev/null @@ -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, - ), - ] diff --git a/authentik/providers/saml/apps.py b/authentik/providers/saml/apps.py index d5cb3b36b..1d6d9c5ed 100644 --- a/authentik/providers/saml/apps.py +++ b/authentik/providers/saml/apps.py @@ -1,5 +1,4 @@ """authentik SAML IdP app config""" -from importlib import import_module from django.apps import AppConfig @@ -11,6 +10,3 @@ class AuthentikProviderSAMLConfig(AppConfig): label = "authentik_providers_saml" verbose_name = "authentik Providers.SAML" mountpoint = "application/saml/" - - def ready(self) -> None: - import_module("authentik.providers.saml.managed") diff --git a/authentik/providers/saml/managed.py b/authentik/providers/saml/managed.py deleted file mode 100644 index 2b5f284cc..000000000 --- a/authentik/providers/saml/managed.py +++ /dev/null @@ -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="", - ), - ] diff --git a/authentik/providers/saml/tests/test_auth_n_request.py b/authentik/providers/saml/tests/test_auth_n_request.py index 61112bf14..44a46564b 100644 --- a/authentik/providers/saml/tests/test_auth_n_request.py +++ b/authentik/providers/saml/tests/test_auth_n_request.py @@ -4,7 +4,7 @@ from base64 import b64encode from django.http.request import QueryDict from django.test import RequestFactory, TestCase -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction @@ -74,8 +74,8 @@ qNAZMq1DqpibfCBg class TestAuthNRequest(TestCase): """Test AuthN Request generator and parser""" + @apply_blueprint("blueprints/system/providers-saml.yaml") def setUp(self): - ObjectManager().run() cert = create_test_cert() self.provider: SAMLProvider = SAMLProvider.objects.create( authorization_flow=create_test_flow(), diff --git a/authentik/providers/saml/tests/test_schema.py b/authentik/providers/saml/tests/test_schema.py index 9d981d86c..9bb5dc6ee 100644 --- a/authentik/providers/saml/tests/test_schema.py +++ b/authentik/providers/saml/tests/test_schema.py @@ -4,7 +4,7 @@ from base64 import b64encode from django.test import RequestFactory, TestCase from lxml import etree # nosec -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.tests.utils import create_test_cert, create_test_flow from authentik.lib.tests.utils import get_request from authentik.lib.xml import lxml_from_string @@ -18,8 +18,8 @@ from authentik.sources.saml.processors.request import RequestProcessor class TestSchema(TestCase): """Test Requests and Responses against schema""" + @apply_blueprint("blueprints/system/providers-saml.yaml") def setUp(self): - ObjectManager().run() cert = create_test_cert() self.provider: SAMLProvider = SAMLProvider.objects.create( authorization_flow=create_test_flow(), diff --git a/authentik/root/celery.py b/authentik/root/celery.py index 05271dc9a..003164b4f 100644 --- a/authentik/root/celery.py +++ b/authentik/root/celery.py @@ -58,6 +58,7 @@ def task_prerun_hook(task_id: str, task, *args, **kwargs): @task_postrun.connect def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs): """Log task_id on worker""" + CTX_TASK_ID.set(...) LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state) @@ -69,6 +70,7 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs): from authentik.events.models import Event, EventAction LOGGER.warning("Task failure", exc=exception) + CTX_TASK_ID.set(...) if before_send({}, {"exc_info": (None, exception, None)}) is not None: Event.new(EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception)).save() @@ -76,7 +78,6 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs): def _get_startup_tasks() -> list[Callable]: """Get all tasks to be run on startup""" from authentik.admin.tasks import clear_update_notifications - from authentik.blueprints.tasks import managed_reconcile from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection from authentik.providers.proxy.tasks import proxy_set_defaults @@ -85,7 +86,6 @@ def _get_startup_tasks() -> list[Callable]: outpost_local_connection, outpost_controller_all, proxy_set_defaults, - managed_reconcile, ] diff --git a/authentik/sources/ldap/apps.py b/authentik/sources/ldap/apps.py index 797f853db..886f71862 100644 --- a/authentik/sources/ldap/apps.py +++ b/authentik/sources/ldap/apps.py @@ -1,16 +1,15 @@ """authentik ldap source config""" -from importlib import import_module - -from django.apps import AppConfig +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikSourceLDAPConfig(AppConfig): +class AuthentikSourceLDAPConfig(ManagedAppConfig): """Authentik ldap app config""" name = "authentik.sources.ldap" label = "authentik_sources_ldap" verbose_name = "authentik Sources.LDAP" + default = True - def ready(self): - import_module("authentik.sources.ldap.signals") - import_module("authentik.sources.ldap.managed") + def reconcile_load_sources_ldap_signals(self): + """Load sources.ldap signals""" + self.import_module("authentik.sources.ldap.signals") diff --git a/authentik/sources/ldap/managed.py b/authentik/sources/ldap/managed.py deleted file mode 100644 index 98c4e383e..000000000 --- a/authentik/sources/ldap/managed.py +++ /dev/null @@ -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')", - ), - ] diff --git a/authentik/sources/ldap/tests/test_auth.py b/authentik/sources/ldap/tests/test_auth.py index 41429d82d..8923e3fa2 100644 --- a/authentik/sources/ldap/tests/test_auth.py +++ b/authentik/sources/ldap/tests/test_auth.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, PropertyMock, patch from django.db.models import Q from django.test import TestCase -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.models import User from authentik.lib.generators import generate_key from authentik.sources.ldap.auth import LDAPBackend @@ -19,8 +19,8 @@ LDAP_PASSWORD = generate_key() class LDAPSyncTests(TestCase): """LDAP Sync tests""" + @apply_blueprint("blueprints/system/sources-ldap.yaml") def setUp(self): - ObjectManager().run() self.source = LDAPSource.objects.create( name="ldap", slug="ldap", diff --git a/authentik/sources/ldap/tests/test_sync.py b/authentik/sources/ldap/tests/test_sync.py index 5caa96e91..b9664eb04 100644 --- a/authentik/sources/ldap/tests/test_sync.py +++ b/authentik/sources/ldap/tests/test_sync.py @@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch from django.db.models import Q from django.test import TestCase -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints import apply_blueprint from authentik.core.models import Group, User from authentik.core.tests.utils import create_test_admin_user from authentik.events.models import Event, EventAction @@ -23,8 +23,8 @@ LDAP_PASSWORD = generate_key() class LDAPSyncTests(TestCase): """LDAP Sync tests""" + @apply_blueprint("blueprints/system/sources-ldap.yaml") def setUp(self): - ObjectManager().run() self.source: LDAPSource = LDAPSource.objects.create( name="ldap", slug="ldap", diff --git a/authentik/sources/oauth/apps.py b/authentik/sources/oauth/apps.py index a698cff7a..b22655734 100644 --- a/authentik/sources/oauth/apps.py +++ b/authentik/sources/oauth/apps.py @@ -1,9 +1,8 @@ """authentik oauth_client config""" -from importlib import import_module - -from django.apps import AppConfig from structlog.stdlib import get_logger +from authentik.blueprints.manager import ManagedAppConfig + LOGGER = get_logger() AUTHENTIK_SOURCES_OAUTH_TYPES = [ @@ -21,18 +20,19 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [ ] -class AuthentikSourceOAuthConfig(AppConfig): +class AuthentikSourceOAuthConfig(ManagedAppConfig): """authentik source.oauth config""" name = "authentik.sources.oauth" label = "authentik_sources_oauth" verbose_name = "authentik Sources.OAuth" mountpoint = "source/oauth/" + default = True - def ready(self): + def reconcile_sources_loaded(self): """Load source_types from config file""" for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES: try: - import_module(source_type) + self.import_module(source_type) except ImportError as exc: LOGGER.warning("Failed to load OAuth Source", exc=exc) diff --git a/authentik/sources/saml/apps.py b/authentik/sources/saml/apps.py index 746d2d45b..a9453817f 100644 --- a/authentik/sources/saml/apps.py +++ b/authentik/sources/saml/apps.py @@ -1,17 +1,16 @@ """Authentik SAML app config""" - -from importlib import import_module - -from django.apps import AppConfig +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikSourceSAMLConfig(AppConfig): +class AuthentikSourceSAMLConfig(ManagedAppConfig): """authentik saml source app config""" name = "authentik.sources.saml" label = "authentik_sources_saml" verbose_name = "authentik Sources.SAML" mountpoint = "source/saml/" + default = True - def ready(self): - import_module("authentik.sources.saml.signals") + def reconcile_load_sources_saml_signals(self): + """Load sources.saml signals""" + self.import_module("authentik.sources.saml.signals") diff --git a/authentik/stages/authenticator_static/apps.py b/authentik/stages/authenticator_static/apps.py index 1edd2b873..9865cb20b 100644 --- a/authentik/stages/authenticator_static/apps.py +++ b/authentik/stages/authenticator_static/apps.py @@ -1,15 +1,15 @@ """Authenticator Static stage""" -from importlib import import_module - -from django.apps import AppConfig +from authentik.blueprints.manager import ManagedAppConfig -class AuthentikStageAuthenticatorStaticConfig(AppConfig): +class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig): """Authenticator Static stage""" name = "authentik.stages.authenticator_static" label = "authentik_stages_authenticator_static" verbose_name = "authentik Stages.Authenticator.Static" + default = True - def ready(self): - import_module("authentik.stages.authenticator_static.signals") + def reconcile_load_stages_authenticator_static_signals(self): + """Load stages.authenticator_static signals""" + self.import_module("authentik.stages.authenticator_static.signals") diff --git a/authentik/stages/email/apps.py b/authentik/stages/email/apps.py index c3c1c5788..8fd2d8876 100644 --- a/authentik/stages/email/apps.py +++ b/authentik/stages/email/apps.py @@ -1,30 +1,26 @@ """authentik email stage config""" -from importlib import import_module - -from django.apps import AppConfig -from django.db import ProgrammingError from django.template.exceptions import TemplateDoesNotExist from django.template.loader import get_template from structlog.stdlib import get_logger +from authentik.blueprints.manager import ManagedAppConfig + LOGGER = get_logger() -class AuthentikStageEmailConfig(AppConfig): +class AuthentikStageEmailConfig(ManagedAppConfig): """authentik email stage config""" name = "authentik.stages.email" label = "authentik_stages_email" verbose_name = "authentik Stages.Email" + default = True - def ready(self): - import_module("authentik.stages.email.tasks") - try: - self.validate_stage_templates() - except ProgrammingError: - pass + def reconcile_load_stages_emails_tasks(self): + """Load stages.emails tasks""" + self.import_module("authentik.stages.email.tasks") - def validate_stage_templates(self): + def reconcile_stage_templates_valid(self): """Ensure all stage's templates actually exist""" from authentik.events.models import Event, EventAction from authentik.stages.email.models import EmailStage, EmailTemplates diff --git a/blueprints/default/20-flow-default-source-enrollment.yaml b/blueprints/default/20-flow-default-source-enrollment.yaml index 248445271..d226dce39 100644 --- a/blueprints/default/20-flow-default-source-enrollment.yaml +++ b/blueprints/default/20-flow-default-source-enrollment.yaml @@ -27,7 +27,7 @@ entries: expression: | # Check if we''ve not been given a username by the external IdP # and trigger the enrollment flow - return ''username'' not in context.get(''prompt_data'', {}) + return 'username' not in context.get('prompt_data', {}) meta_model_name: authentik_policies_expression.expressionpolicy identifiers: name: default-source-enrollment-if-username @@ -78,7 +78,7 @@ entries: order: 0 stage: !KeyOf default-source-enrollment-prompt target: !KeyOf flow - id: prompt-binding + id: prompt-binding model: authentik_flows.flowstagebinding - attrs: evaluate_on_plan: true diff --git a/blueprints/default/20-flow-default-source-pre-authentication.yaml b/blueprints/default/20-flow-default-source-pre-authentication.yaml index a574cf0d9..b6e764184 100644 --- a/blueprints/default/20-flow-default-source-pre-authentication.yaml +++ b/blueprints/default/20-flow-default-source-pre-authentication.yaml @@ -5,7 +5,7 @@ entries: layout: stacked name: Pre-Authentication policy_engine_mode: any - title: '' + title: Pre-authentication identifiers: slug: default-source-pre-authentication model: authentik_flows.flow diff --git a/blueprints/default/30-flow-default-user-settings-flow.yaml b/blueprints/default/30-flow-default-user-settings-flow.yaml index 83bedffd6..ac76eb4ac 100644 --- a/blueprints/default/30-flow-default-user-settings-flow.yaml +++ b/blueprints/default/30-flow-default-user-settings-flow.yaml @@ -3,9 +3,9 @@ entries: compatibility_mode: false designation: stage_configuration layout: stacked - name: Update your info + name: User settings policy_engine_mode: any - title: '' + title: Update your info identifiers: slug: default-user-settings-flow model: authentik_flows.flow @@ -108,9 +108,9 @@ entries: return True meta_model_name: authentik_policies_expression.expressionpolicy - name: default-user-settings-authorization identifiers: name: default-user-settings-authorization + id: default-user-settings-authorization model: authentik_policies_expression.expressionpolicy - attrs: create_users_as_inactive: false diff --git a/blueprints/example/flows-enrollment-2-stage.yaml b/blueprints/example/flows-enrollment-2-stage.yaml index af89ada4c..db6810fb4 100644 --- a/blueprints/example/flows-enrollment-2-stage.yaml +++ b/blueprints/example/flows-enrollment-2-stage.yaml @@ -76,7 +76,6 @@ entries: - !KeyOf prompt-field-password - !KeyOf prompt-field-password-repeat - identifiers: - pk: !KeyOf default-enrollment-user-login name: default-enrollment-user-login id: default-enrollment-user-login model: authentik_stages_user_login.userloginstage diff --git a/blueprints/example/flows-login-2fa.yaml b/blueprints/example/flows-login-2fa.yaml index c576a6e38..05a80ad8b 100644 --- a/blueprints/example/flows-login-2fa.yaml +++ b/blueprints/example/flows-login-2fa.yaml @@ -39,7 +39,6 @@ entries: model: authentik_stages_authenticator_validate.AuthenticatorValidateStage attrs: {} - identifiers: - pk: !KeyOf default-authentication-password name: default-authentication-password id: default-authentication-password model: authentik_stages_password.passwordstage diff --git a/blueprints/example/flows-recovery-email-verification.yaml b/blueprints/example/flows-recovery-email-verification.yaml index 839d978dd..6084d242b 100644 --- a/blueprints/example/flows-recovery-email-verification.yaml +++ b/blueprints/example/flows-recovery-email-verification.yaml @@ -93,7 +93,7 @@ entries: session_duration: seconds=0 - identifiers: name: Change your password - name: stages-prompt-password + id: stages-prompt-password model: authentik_stages_prompt.promptstage attrs: fields: diff --git a/blueprints/system/providers-oauth2.yaml b/blueprints/system/providers-oauth2.yaml new file mode 100644 index 000000000..ed5f3e4e4 --- /dev/null +++ b/blueprints/system/providers-oauth2.yaml @@ -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()], + } diff --git a/blueprints/system/providers-proxy.yaml b/blueprints/system/providers-proxy.yaml new file mode 100644 index 000000000..221eb1cd0 --- /dev/null +++ b/blueprints/system/providers-proxy.yaml @@ -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, + } + } diff --git a/blueprints/system/providers-saml.yaml b/blueprints/system/providers-saml.yaml new file mode 100644 index 000000000..60a9bddee --- /dev/null +++ b/blueprints/system/providers-saml.yaml @@ -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 diff --git a/blueprints/system/sources-ldap.yaml b/blueprints/system/sources-ldap.yaml new file mode 100644 index 000000000..0282d9afd --- /dev/null +++ b/blueprints/system/sources-ldap.yaml @@ -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') diff --git a/schema.yml b/schema.yml index e495ebc45..3eda092b6 100644 --- a/schema.yml +++ b/schema.yml @@ -20866,6 +20866,11 @@ components: type: object description: Info about a single blueprint instance file properties: + pk: + type: string + format: uuid + readOnly: true + title: Instance uuid name: type: string path: @@ -20877,15 +20882,26 @@ components: type: string format: date-time readOnly: true + last_applied_hash: + type: string + readOnly: true status: $ref: '#/components/schemas/BlueprintInstanceStatusEnum' enabled: type: boolean + managed_models: + type: array + items: + type: string + readOnly: true required: - context - last_applied + - last_applied_hash + - managed_models - name - path + - pk - status BlueprintInstanceRequest: type: object @@ -20914,6 +20930,7 @@ components: - successful - warning - error + - orphaned - unknown type: string Cache: diff --git a/scripts/generate_config.py b/scripts/generate_config.py new file mode 100644 index 000000000..4d3d474e9 --- /dev/null +++ b/scripts/generate_config.py @@ -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, + ) diff --git a/tests/e2e/test_flows_authenticators.py b/tests/e2e/test_flows_authenticators.py index 4b0d25e1f..e872136e9 100644 --- a/tests/e2e/test_flows_authenticators.py +++ b/tests/e2e/test_flows_authenticators.py @@ -13,11 +13,11 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.wait import WebDriverWait -from authentik.flows.models import Flow, FlowStageBinding +from authentik.blueprints import apply_blueprint +from authentik.flows.models import Flow from authentik.stages.authenticator_static.models import AuthenticatorStaticStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage -from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage -from tests.e2e.utils import SeleniumTestCase, apply_migration, retry +from tests.e2e.utils import SeleniumTestCase, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -25,18 +25,16 @@ class TestFlowsAuthenticator(SeleniumTestCase): """test flow with otp stages""" @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_totp_validate(self): """test flow with otp stages""" - sleep(1) # Setup TOTP Device device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6) flow: Flow = Flow.objects.get(slug="default-authentication-flow") - FlowStageBinding.objects.create( - target=flow, order=30, stage=AuthenticatorValidateStage.objects.create() - ) self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) self.login() @@ -47,16 +45,17 @@ class TestFlowsAuthenticator(SeleniumTestCase): flow_executor = self.get_shadow_root("ak-flow-executor") validation_stage = self.get_shadow_root("ak-stage-authenticator-validate", flow_executor) code_stage = self.get_shadow_root("ak-stage-authenticator-validate-code", validation_stage) - code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(totp.token()) code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER) self.wait_for_url(self.if_user_url("/library")) self.assert_user(self.user) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_stages_authenticator_totp", "0006_default_setup_flow") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint("blueprints/default/20-flow-default-authenticator-totp-setup.yaml") def test_totp_setup(self): """test TOTP Setup stage""" flow: Flow = Flow.objects.get(slug="default-authentication-flow") @@ -98,9 +97,11 @@ class TestFlowsAuthenticator(SeleniumTestCase): self.assertTrue(TOTPDevice.objects.filter(user=self.user, confirmed=True).exists()) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_stages_authenticator_static", "0005_default_setup_flow") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint("blueprints/default/20-flow-default-authenticator-static-setup.yaml") def test_static_setup(self): """test Static OTP Setup stage""" flow: Flow = Flow.objects.get(slug="default-authentication-flow") diff --git a/tests/e2e/test_flows_enroll.py b/tests/e2e/test_flows_enroll.py index b83128424..06c708f40 100644 --- a/tests/e2e/test_flows_enroll.py +++ b/tests/e2e/test_flows_enroll.py @@ -9,6 +9,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.wait import WebDriverWait +from authentik.blueprints import apply_blueprint from authentik.core.models import User from authentik.core.tests.utils import create_test_flow from authentik.flows.models import FlowDesignation, FlowStageBinding @@ -18,7 +19,7 @@ from authentik.stages.identification.models import IdentificationStage from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_write.models import UserWriteStage -from tests.e2e.utils import SeleniumTestCase, apply_migration, retry +from tests.e2e.utils import SeleniumTestCase, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -39,8 +40,10 @@ class TestFlowsEnroll(SeleniumTestCase): } @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_enroll_2_step(self): """Test 2-step enroll flow""" # First stage fields @@ -103,8 +106,10 @@ class TestFlowsEnroll(SeleniumTestCase): self.assertEqual(user.email, "foo@bar.baz") @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_enroll_email(self): """Test enroll with Email verification""" # First stage fields diff --git a/tests/e2e/test_flows_login.py b/tests/e2e/test_flows_login.py index 1bc81d800..3a0db6538 100644 --- a/tests/e2e/test_flows_login.py +++ b/tests/e2e/test_flows_login.py @@ -2,7 +2,8 @@ from sys import platform from unittest.case import skipUnless -from tests.e2e.utils import SeleniumTestCase, apply_migration, retry +from authentik.blueprints import apply_blueprint +from tests.e2e.utils import SeleniumTestCase, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -10,8 +11,10 @@ class TestFlowsLogin(SeleniumTestCase): """test default login flow""" @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_login(self): """test default login flow""" self.driver.get( diff --git a/tests/e2e/test_flows_stage_setup.py b/tests/e2e/test_flows_stage_setup.py index baabc6766..e9b657a71 100644 --- a/tests/e2e/test_flows_stage_setup.py +++ b/tests/e2e/test_flows_stage_setup.py @@ -5,11 +5,12 @@ from unittest.case import skipUnless from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys +from authentik.blueprints import apply_blueprint from authentik.core.models import User from authentik.flows.models import Flow, FlowDesignation from authentik.lib.generators import generate_key from authentik.stages.password.models import PasswordStage -from tests.e2e.utils import SeleniumTestCase, apply_migration, retry +from tests.e2e.utils import SeleniumTestCase, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -17,9 +18,11 @@ class TestFlowsStageSetup(SeleniumTestCase): """test stage setup flows""" @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_stages_password", "0002_passwordstage_change_flow") + @apply_blueprint("blueprints/default/0-flow-password-change.yaml") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_password_change(self): """test password change flow""" # Ensure that password stage has change_flow set diff --git a/tests/e2e/test_provider_ldap.py b/tests/e2e/test_provider_ldap.py index 53c9dbae2..1ef2a48dd 100644 --- a/tests/e2e/test_provider_ldap.py +++ b/tests/e2e/test_provider_ldap.py @@ -10,13 +10,14 @@ from guardian.shortcuts import get_anonymous_user from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server from ldap3.core.exceptions import LDAPInvalidCredentialsResult +from authentik.blueprints import apply_blueprint from authentik.core.models import Application, User from authentik.events.models import Event, EventAction from authentik.flows.models import Flow -from authentik.outposts.managed import MANAGED_OUTPOST +from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.models import Outpost, OutpostConfig, OutpostType from authentik.providers.ldap.models import APIAccessMode, LDAPProvider -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -81,8 +82,10 @@ class TestProviderLDAP(SeleniumTestCase): return outpost @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_ldap_bind_success(self): """Test simple bind""" self._prepare() @@ -106,8 +109,10 @@ class TestProviderLDAP(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_ldap_bind_success_ssl(self): """Test simple bind with ssl""" self._prepare() @@ -131,8 +136,10 @@ class TestProviderLDAP(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) def test_ldap_bind_fail(self): """Test simple bind (failed)""" self._prepare() @@ -154,8 +161,11 @@ class TestProviderLDAP(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @reconcile_app("authentik_outposts") def test_ldap_bind_search(self): """Test simple bind + search""" outpost = self._prepare() diff --git a/tests/e2e/test_provider_oauth2_github.py b/tests/e2e/test_provider_oauth2_github.py index 091d87322..a5e2dd4d8 100644 --- a/tests/e2e/test_provider_oauth2_github.py +++ b/tests/e2e/test_provider_oauth2_github.py @@ -8,13 +8,14 @@ from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.flows.models import Flow from authentik.lib.generators import generate_id, generate_key from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider -from tests.e2e.utils import SeleniumTestCase, apply_migration, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -56,10 +57,18 @@ class TestProviderOAuth2Github(SeleniumTestCase): } @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_consent_implied(self): """test OAuth Provider flow (default authorization flow with implied consent)""" # Bootstrap all needed objects @@ -104,10 +113,18 @@ class TestProviderOAuth2Github(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_consent_explicit(self): """test OAuth Provider flow (default authorization flow with explicit consent)""" # Bootstrap all needed objects @@ -171,10 +188,15 @@ class TestProviderOAuth2Github(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_denied(self): """test OAuth Provider flow (default authorization flow, denied)""" # Bootstrap all needed objects diff --git a/tests/e2e/test_provider_oauth2_grafana.py b/tests/e2e/test_provider_oauth2_grafana.py index a99009ed0..5c301bd35 100644 --- a/tests/e2e/test_provider_oauth2_grafana.py +++ b/tests/e2e/test_provider_oauth2_grafana.py @@ -8,6 +8,7 @@ from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert from authentik.flows.models import Flow @@ -20,7 +21,7 @@ from authentik.providers.oauth2.constants import ( SCOPE_OPENID_PROFILE, ) from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -65,10 +66,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): } @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_redirect_uri_error(self): """test OpenID Provider flow (invalid redirect URI, check error message)""" sleep(1) @@ -106,11 +115,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_consent_implied(self): """test OpenID Provider flow (default authorization flow with implied consent)""" sleep(1) @@ -161,11 +177,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_logout(self): """test OpenID Provider flow with logout""" sleep(1) @@ -225,11 +248,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): self.driver.find_element(By.ID, "logout").click() @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_consent_explicit(self): """test OpenID Provider flow (default authorization flow with explicit consent)""" sleep(1) @@ -298,10 +328,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_denied(self): """test OpenID Provider flow (default authorization with access deny)""" sleep(1) diff --git a/tests/e2e/test_provider_oauth2_oidc.py b/tests/e2e/test_provider_oauth2_oidc.py index 06a44ce8f..af05eb632 100644 --- a/tests/e2e/test_provider_oauth2_oidc.py +++ b/tests/e2e/test_provider_oauth2_oidc.py @@ -10,6 +10,7 @@ from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert from authentik.flows.models import Flow @@ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import ( SCOPE_OPENID_PROFILE, ) from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -64,10 +65,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): sleep(1) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_redirect_uri_error(self): """test OpenID Provider flow (invalid redirect URI, check error message)""" sleep(1) @@ -105,11 +111,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def test_authorization_consent_implied(self): """test OpenID Provider flow (default authorization flow with implied consent)""" sleep(1) @@ -155,11 +166,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): self.assertEqual(body["UserInfo"]["email"], self.user.email) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def test_authorization_consent_explicit(self): """test OpenID Provider flow (default authorization flow with explicit consent)""" sleep(1) @@ -220,10 +236,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): self.assertEqual(body["UserInfo"]["email"], self.user.email) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_denied(self): """test OpenID Provider flow (default authorization with access deny)""" sleep(1) diff --git a/tests/e2e/test_provider_oauth2_oidc_implicit.py b/tests/e2e/test_provider_oauth2_oidc_implicit.py index fa64165ea..19c743ebb 100644 --- a/tests/e2e/test_provider_oauth2_oidc_implicit.py +++ b/tests/e2e/test_provider_oauth2_oidc_implicit.py @@ -10,6 +10,7 @@ from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert from authentik.flows.models import Flow @@ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import ( SCOPE_OPENID_PROFILE, ) from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -64,10 +65,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): sleep(1) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_redirect_uri_error(self): """test OpenID Provider flow (invalid redirect URI, check error message)""" sleep(1) @@ -105,11 +111,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def test_authorization_consent_implied(self): """test OpenID Provider flow (default authorization flow with implied consent)""" sleep(1) @@ -150,11 +161,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): self.assertEqual(body["profile"]["email"], self.user.email) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") + @apply_blueprint("blueprints/system/providers-oauth2.yaml") def test_authorization_consent_explicit(self): """test OpenID Provider flow (default authorization flow with explicit consent)""" sleep(1) @@ -211,10 +227,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): self.assertEqual(body["profile"]["email"], self.user.email) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_authorization_denied(self): """test OpenID Provider flow (default authorization with access deny)""" sleep(1) diff --git a/tests/e2e/test_provider_proxy.py b/tests/e2e/test_provider_proxy.py index c75be5d5d..037a0c36d 100644 --- a/tests/e2e/test_provider_proxy.py +++ b/tests/e2e/test_provider_proxy.py @@ -11,12 +11,13 @@ from docker.models.containers import Container from selenium.webdriver.common.by import By from authentik import __version__ +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.flows.models import Flow from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType from authentik.outposts.tasks import outpost_local_connection from authentik.providers.proxy.models import ProxyProvider -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -53,11 +54,19 @@ class TestProviderProxy(SeleniumTestCase): return container @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-oauth2.yaml", + "blueprints/system/providers-proxy.yaml", + ) + @reconcile_app("authentik_crypto") def test_proxy_simple(self): """Test simple outpost setup with single provider""" # set additionalHeaders to test later @@ -116,11 +125,15 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase): """Test Proxy connectivity over websockets""" @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @reconcile_app("authentik_crypto") def test_proxy_connectivity(self): """Test proxy connectivity over websocket""" outpost_local_connection() diff --git a/tests/e2e/test_provider_saml.py b/tests/e2e/test_provider_saml.py index 3beef3062..f0302bcc5 100644 --- a/tests/e2e/test_provider_saml.py +++ b/tests/e2e/test_provider_saml.py @@ -10,6 +10,7 @@ from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec +from authentik.blueprints import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert from authentik.flows.models import Flow @@ -17,7 +18,7 @@ from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider from authentik.sources.saml.processors.constants import SAML_BINDING_POST -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry @skipUnless(platform.startswith("linux"), "requires local docker") @@ -63,11 +64,18 @@ class TestProviderSAML(SeleniumTestCase): sleep(1) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-saml.yaml", + ) + @reconcile_app("authentik_crypto") def test_sp_initiated_implicit(self): """test SAML Provider flow SP-initiated flow (implicit consent)""" # Bootstrap all needed objects @@ -125,11 +133,18 @@ class TestProviderSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-saml.yaml", + ) + @reconcile_app("authentik_crypto") def test_sp_initiated_explicit(self): """test SAML Provider flow SP-initiated flow (explicit consent)""" # Bootstrap all needed objects @@ -202,11 +217,18 @@ class TestProviderSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-saml.yaml", + ) + @reconcile_app("authentik_crypto") def test_sp_initiated_explicit_post(self): """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" # Bootstrap all needed objects @@ -279,11 +301,18 @@ class TestProviderSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-saml.yaml", + ) + @reconcile_app("authentik_crypto") def test_idp_initiated_implicit(self): """test SAML Provider flow IdP-initiated flow (implicit consent)""" # Bootstrap all needed objects @@ -347,11 +376,18 @@ class TestProviderSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0010_provider_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/system/providers-saml.yaml", + ) + @reconcile_app("authentik_crypto") def test_sp_initiated_denied(self): """test SAML Provider flow SP-initiated flow (Policy denies access)""" # Bootstrap all needed objects diff --git a/tests/e2e/test_source_oauth.py b/tests/e2e/test_source_oauth.py index f1853a107..d59302c17 100644 --- a/tests/e2e/test_source_oauth.py +++ b/tests/e2e/test_source_oauth.py @@ -13,6 +13,7 @@ from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.wait import WebDriverWait from yaml import safe_dump +from authentik.blueprints import apply_blueprint from authentik.core.models import User from authentik.flows.models import Flow from authentik.lib.generators import generate_id, generate_key @@ -20,7 +21,7 @@ from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.views.callback import OAuthCallback from authentik.stages.identification.models import IdentificationStage -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, retry CONFIG_PATH = "/tmp/dex.yml" # nosec @@ -141,11 +142,19 @@ class TestSourceOAuth2(SeleniumTestCase): ident_stage.save() @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-source-authentication.yaml", + "blueprints/default/20-flow-default-source-enrollment.yaml", + "blueprints/default/20-flow-default-source-pre-authentication.yaml", + ) def test_oauth_enroll(self): """test OAuth Source With With OIDC""" self.create_objects() @@ -190,11 +199,14 @@ class TestSourceOAuth2(SeleniumTestCase): self.assert_user(User(username="foo", name="admin", email="admin@example.com")) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", + "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", + ) def test_oauth_enroll_auth(self): """test OAuth Source With With OIDC (enroll and authenticate again)""" self.test_oauth_enroll() @@ -279,11 +291,15 @@ class TestSourceOAuth1(SeleniumTestCase): ident_stage.save() @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-source-authentication.yaml", + "blueprints/default/20-flow-default-source-enrollment.yaml", + "blueprints/default/20-flow-default-source-pre-authentication.yaml", + ) def test_oauth_enroll(self): """test OAuth Source With With OIDC""" self.create_objects() diff --git a/tests/e2e/test_source_saml.py b/tests/e2e/test_source_saml.py index 524399f47..88625ccc8 100644 --- a/tests/e2e/test_source_saml.py +++ b/tests/e2e/test_source_saml.py @@ -11,12 +11,13 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.wait import WebDriverWait +from authentik.blueprints import apply_blueprint from authentik.core.models import User from authentik.crypto.models import CertificateKeyPair from authentik.flows.models import Flow from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource from authentik.stages.identification.models import IdentificationStage -from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry +from tests.e2e.utils import SeleniumTestCase, retry IDP_CERT = """-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV @@ -94,12 +95,15 @@ class TestSourceSAML(SeleniumTestCase): } @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-source-authentication.yaml", + "blueprints/default/20-flow-default-source-enrollment.yaml", + "blueprints/default/20-flow-default-source-pre-authentication.yaml", + ) def test_idp_redirect(self): """test SAML Source With redirect binding""" # Bootstrap all needed objects @@ -161,12 +165,15 @@ class TestSourceSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-source-authentication.yaml", + "blueprints/default/20-flow-default-source-enrollment.yaml", + "blueprints/default/20-flow-default-source-pre-authentication.yaml", + ) def test_idp_post(self): """test SAML Source With post binding""" # Bootstrap all needed objects @@ -241,12 +248,15 @@ class TestSourceSAML(SeleniumTestCase): ) @retry() - @apply_migration("authentik_flows", "0008_default_flows") - @apply_migration("authentik_flows", "0011_flow_title") - @apply_migration("authentik_flows", "0009_source_flows") - @apply_migration("authentik_crypto", "0002_create_self_signed_kp") - @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") - @object_manager + @apply_blueprint( + "blueprints/default/10-flow-default-authentication-flow.yaml", + "blueprints/default/10-flow-default-invalidation-flow.yaml", + ) + @apply_blueprint( + "blueprints/default/20-flow-default-source-authentication.yaml", + "blueprints/default/20-flow-default-source-enrollment.yaml", + "blueprints/default/20-flow-default-source-pre-authentication.yaml", + ) def test_idp_post_auto(self): """test SAML Source With post binding (auto redirect)""" # Bootstrap all needed objects diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index f90838f4f..6fe1d824d 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -10,7 +10,6 @@ from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.db import connection from django.db.migrations.loader import MigrationLoader -from django.db.migrations.operations.special import RunPython from django.test.testcases import TransactionTestCase from django.urls import reverse from docker import DockerClient, from_env @@ -25,7 +24,7 @@ from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.ui import WebDriverWait from structlog.stdlib import get_logger -from authentik.blueprints.manager import ObjectManager +from authentik.blueprints.manager import ManagedAppConfig from authentik.core.api.users import UserSerializer from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user @@ -193,37 +192,22 @@ def get_loader(): return MigrationLoader(connection) -def apply_migration(app_name: str, migration_name: str): - """Re-apply migrations that create objects using RunPython before test cases""" +def reconcile_app(app_name: str): + """Re-reconcile AppConfig methods""" - def wrapper_outter(func: Callable): - """Retry test multiple times""" + def wrapper_outer(func: Callable): + """Re-reconcile AppConfig methods""" @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): - migration = get_loader().get_migration(app_name, migration_name) - with connection.schema_editor() as schema_editor: - for operation in migration.operations: - if not isinstance(operation, RunPython): - continue - operation.code(apps, schema_editor) + config = apps.get_app_config(app_name) + if isinstance(config, ManagedAppConfig): + config.reconcile() return func(self, *args, **kwargs) return wrapper - return wrapper_outter - - -def object_manager(func: Callable): - """Run objectmanager before a test function""" - - @wraps(func) - def wrapper(*args, **kwargs): - """Run objectmanager before a test function""" - ObjectManager().run() - return func(*args, **kwargs) - - return wrapper + return wrapper_outer def retry(max_retires=RETRIES, exceptions=None): diff --git a/website/developer-docs/setup/full-dev-environment.md b/website/developer-docs/setup/full-dev-environment.md index 1b4b4269a..e6057108f 100644 --- a/website/developer-docs/setup/full-dev-environment.md +++ b/website/developer-docs/setup/full-dev-environment.md @@ -13,7 +13,7 @@ title: Full development environment ## 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. ## 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 ``` -To configure authentik to use the local databases, create a file in the authentik directory called `local.env.yml`, with the following contents - -```yaml -debug: true -postgresql: - user: postgres - -log_level: debug -secret_key: "A long key you can generate with `pwgen 40 1` for example" -``` +To configure authentik to use the local databases, we need a local config file. This file can be generated by running `make gen-dev-config`. To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream. @@ -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`. -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. To format the frontend code, run `make web`.