blueprints: migrate from managed (#3338)
* test all bundled blueprints Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix empty title Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix default blueprints Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add script to generate dev config Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * migrate managed to blueprints Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add more to blueprint instance Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * migrated away from ObjectManager Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix lint errors Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * migrate things Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * migrate tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix some tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix a bit more Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix more tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * whops Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix missing name Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * *sigh* Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix more tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add tasks Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * scheduled Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * run discovery on start Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * oops this test should stay Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -73,7 +73,7 @@ RUN apt-get update && \ | |||||||
|     apt-get clean && \ |     apt-get clean && \ | ||||||
|     rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \ |     rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \ | ||||||
|     adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \ |     adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \ | ||||||
|     mkdir -p /certs /media && \ |     mkdir -p /certs /media /blueprints && \ | ||||||
|     mkdir -p /authentik/.ssh && \ |     mkdir -p /authentik/.ssh && \ | ||||||
|     chown authentik:authentik /certs /media /authentik/.ssh |     chown authentik:authentik /certs /media /authentik/.ssh | ||||||
|  |  | ||||||
| @ -82,7 +82,8 @@ COPY ./pyproject.toml / | |||||||
| COPY ./xml /xml | COPY ./xml /xml | ||||||
| COPY ./tests /tests | COPY ./tests /tests | ||||||
| COPY ./manage.py / | COPY ./manage.py / | ||||||
| COPY ./blueprints/default /blueprints | COPY ./blueprints/default /blueprints/default | ||||||
|  | COPY ./blueprints/system /blueprints/system | ||||||
| COPY ./lifecycle/ /lifecycle | COPY ./lifecycle/ /lifecycle | ||||||
| COPY --from=builder /work/authentik /authentik-proxy | COPY --from=builder /work/authentik /authentik-proxy | ||||||
| COPY --from=web-builder /work/web/dist/ /web/dist/ | COPY --from=web-builder /work/web/dist/ /web/dist/ | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
								
							| @ -33,8 +33,8 @@ test: | |||||||
| 	coverage report | 	coverage report | ||||||
|  |  | ||||||
| lint-fix: | lint-fix: | ||||||
| 	isort authentik tests lifecycle | 	isort authentik tests scripts lifecycle | ||||||
| 	black authentik tests lifecycle | 	black authentik tests scripts lifecycle | ||||||
| 	codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \ | 	codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \ | ||||||
| 		authentik \ | 		authentik \ | ||||||
| 		internal \ | 		internal \ | ||||||
| @ -91,6 +91,9 @@ gen-client-go: | |||||||
| 	go mod edit -replace goauthentik.io/api/v3=./gen-go-api | 	go mod edit -replace goauthentik.io/api/v3=./gen-go-api | ||||||
| 	rm -rf config.yaml ./templates/ | 	rm -rf config.yaml ./templates/ | ||||||
|  |  | ||||||
|  | gen-dev-config: | ||||||
|  | 	python -m scripts.generate_config | ||||||
|  |  | ||||||
| gen: gen-build gen-clean gen-client-web | gen: gen-build gen-clean gen-client-web | ||||||
|  |  | ||||||
| migrate: | migrate: | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ from rest_framework.response import Response | |||||||
| from rest_framework.views import APIView | from rest_framework.views import APIView | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.models import Outpost | from authentik.outposts.models import Outpost | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,19 +1,20 @@ | |||||||
| """authentik admin app config""" | """authentik admin app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from prometheus_client import Gauge, Info | from prometheus_client import Gauge, Info | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| PROM_INFO = Info("authentik_version", "Currently running authentik version") | PROM_INFO = Info("authentik_version", "Currently running authentik version") | ||||||
| GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") | GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikAdminConfig(AppConfig): | class AuthentikAdminConfig(ManagedAppConfig): | ||||||
|     """authentik admin app config""" |     """authentik admin app config""" | ||||||
|  |  | ||||||
|     name = "authentik.admin" |     name = "authentik.admin" | ||||||
|     label = "authentik_admin" |     label = "authentik_admin" | ||||||
|     verbose_name = "authentik Admin" |     verbose_name = "authentik Admin" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_admin_signals(self): | ||||||
|         import_module("authentik.admin.signals") |         """Load admin signals""" | ||||||
|  |         self.import_module("authentik.admin.signals") | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| """test admin api""" | """test admin api""" | ||||||
| from json import loads | from json import loads | ||||||
|  |  | ||||||
|  | from django.apps import apps | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
| from authentik.blueprints.tasks import managed_reconcile |  | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.core.tasks import clean_expired_models | from authentik.core.tasks import clean_expired_models | ||||||
| from authentik.events.monitored_tasks import TaskResultStatus | from authentik.events.monitored_tasks import TaskResultStatus | ||||||
| @ -95,7 +95,6 @@ class TestAdminAPI(TestCase): | |||||||
|  |  | ||||||
|     def test_system(self): |     def test_system(self): | ||||||
|         """Test system API""" |         """Test system API""" | ||||||
|         # pyright: reportGeneralTypeIssues=false |         apps.get_app_config("authentik_outposts").reconcile_embedded_outpost() | ||||||
|         managed_reconcile()  # pylint: disable=no-value-for-parameter |  | ||||||
|         response = self.client.get(reverse("authentik_api:admin_system")) |         response = self.client.get(reverse("authentik_api:admin_system")) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]: | |||||||
| def token_secret_key(value: str) -> Optional[User]: | def token_secret_key(value: str) -> Optional[User]: | ||||||
|     """Check if the token is the secret key |     """Check if the token is the secret key | ||||||
|     and return the service account for the managed outpost""" |     and return the service account for the managed outpost""" | ||||||
|     from authentik.outposts.managed import MANAGED_OUTPOST |     from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
|  |  | ||||||
|     if value != settings.SECRET_KEY: |     if value != settings.SECRET_KEY: | ||||||
|         return None |         return None | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """Test API Authentication""" | """Test API Authentication""" | ||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
|  |  | ||||||
|  | from django.apps import apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
| @ -10,7 +11,6 @@ from authentik.api.authentication import bearer_auth | |||||||
| from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents | from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents | ||||||
| from authentik.core.tests.utils import create_test_flow | from authentik.core.tests.utils import create_test_flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.outposts.managed import OutpostManager |  | ||||||
| from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API | from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken | from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken | ||||||
|  |  | ||||||
| @ -44,7 +44,7 @@ class TestAPIAuth(TestCase): | |||||||
|         with self.assertRaises(AuthenticationFailed): |         with self.assertRaises(AuthenticationFailed): | ||||||
|             user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) |             user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) | ||||||
|  |  | ||||||
|         OutpostManager().run() |         apps.get_app_config("authentik_outposts").reconcile_embedded_outpost() | ||||||
|         user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) |         user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) | ||||||
|         self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True) |         self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True) | ||||||
|  |  | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -27,13 +27,21 @@ class BlueprintInstanceSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|         model = BlueprintInstance |         model = BlueprintInstance | ||||||
|         fields = [ |         fields = [ | ||||||
|  |             "pk", | ||||||
|             "name", |             "name", | ||||||
|             "path", |             "path", | ||||||
|             "context", |             "context", | ||||||
|             "last_applied", |             "last_applied", | ||||||
|  |             "last_applied_hash", | ||||||
|             "status", |             "status", | ||||||
|             "enabled", |             "enabled", | ||||||
|  |             "managed_models", | ||||||
|         ] |         ] | ||||||
|  |         extra_kwargs = { | ||||||
|  |             "last_applied": {"read_only": True}, | ||||||
|  |             "last_applied_hash": {"read_only": True}, | ||||||
|  |             "managed_models": {"read_only": True}, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class BlueprintInstanceViewSet(ModelViewSet): | class BlueprintInstanceViewSet(ModelViewSet): | ||||||
|  | |||||||
| @ -1,15 +1,22 @@ | |||||||
| """authentik Blueprints app""" | """authentik Blueprints app""" | ||||||
| from django.apps import AppConfig |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikBlueprintsConfig(AppConfig): | class AuthentikBlueprintsConfig(ManagedAppConfig): | ||||||
|     """authentik Blueprints app""" |     """authentik Blueprints app""" | ||||||
|  |  | ||||||
|     name = "authentik.blueprints" |     name = "authentik.blueprints" | ||||||
|     label = "authentik_blueprints" |     label = "authentik_blueprints" | ||||||
|     verbose_name = "authentik Blueprints" |     verbose_name = "authentik Blueprints" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self) -> None: |     def reconcile_load_blueprints_v1_tasks(self): | ||||||
|         from authentik.blueprints.tasks import managed_reconcile |         """Load v1 tasks""" | ||||||
|  |         self.import_module("authentik.blueprints.v1.tasks") | ||||||
|  |  | ||||||
|         managed_reconcile.delay() |     def reconcile_blueprints_discover(self): | ||||||
|  |         """Run blueprint discovery""" | ||||||
|  |         from authentik.blueprints.v1.tasks import blueprints_discover | ||||||
|  |  | ||||||
|  |         blueprints_discover.delay() | ||||||
|  | |||||||
| @ -13,9 +13,9 @@ class Command(BaseCommand):  # pragma: no cover | |||||||
|         for blueprint_path in options.get("blueprints", []): |         for blueprint_path in options.get("blueprints", []): | ||||||
|             with open(blueprint_path, "r", encoding="utf8") as blueprint_file: |             with open(blueprint_path, "r", encoding="utf8") as blueprint_file: | ||||||
|                 importer = Importer(blueprint_file.read()) |                 importer = Importer(blueprint_file.read()) | ||||||
|                 valid = importer.validate() |                 valid, logs = importer.validate() | ||||||
|                 if not valid: |                 if not valid: | ||||||
|                     raise ValueError("blueprint invalid") |                     raise ValueError(f"blueprint invalid: {logs}") | ||||||
|                 importer.apply() |                 importer.apply() | ||||||
|  |  | ||||||
|     def add_arguments(self, parser): |     def add_arguments(self, parser): | ||||||
|  | |||||||
| @ -1,70 +1,37 @@ | |||||||
| """Managed objects manager""" | """Managed objects manager""" | ||||||
| from typing import Callable, Optional | from importlib import import_module | ||||||
|  | from inspect import ismethod | ||||||
|  |  | ||||||
|  | from django.apps import AppConfig | ||||||
|  | from django.db import DatabaseError, ProgrammingError | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.blueprints.models import ManagedModel |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class EnsureOp: | class ManagedAppConfig(AppConfig): | ||||||
|     """Ensure operation, executed as part of an ObjectManager run""" |     """Basic reconciliation logic for apps""" | ||||||
|  |  | ||||||
|     _obj: type[ManagedModel] |     def ready(self) -> None: | ||||||
|     _managed_uid: str |         self.reconcile() | ||||||
|     _kwargs: dict |         return super().ready() | ||||||
|  |  | ||||||
|     def __init__(self, obj: type[ManagedModel], managed_uid: str, **kwargs) -> None: |     def import_module(self, path: str): | ||||||
|         self._obj = obj |         """Load module""" | ||||||
|         self._managed_uid = managed_uid |         import_module(path) | ||||||
|         self._kwargs = kwargs |  | ||||||
|  |  | ||||||
|     def run(self): |     def reconcile(self) -> None: | ||||||
|         """Do the actual ensure action""" |         """reconcile ourselves""" | ||||||
|         raise NotImplementedError |         prefix = "reconcile_" | ||||||
|  |         for meth_name in dir(self): | ||||||
|  |             meth = getattr(self, meth_name) | ||||||
| class EnsureExists(EnsureOp): |             if not ismethod(meth): | ||||||
|     """Ensure object exists, with kwargs as given values""" |                 continue | ||||||
|  |             if not meth_name.startswith(prefix): | ||||||
|     created_callback: Optional[Callable] |                 continue | ||||||
|  |             name = meth_name.replace(prefix, "") | ||||||
|     def __init__( |             try: | ||||||
|         self, |                 meth() | ||||||
|         obj: type[ManagedModel], |                 LOGGER.debug("Successfully reconciled", name=name) | ||||||
|         managed_uid: str, |             except (ProgrammingError, DatabaseError) as exc: | ||||||
|         created_callback: Optional[Callable] = None, |                 LOGGER.debug("Failed to run reconcile", name=name, exc=exc) | ||||||
|         **kwargs, |  | ||||||
|     ) -> None: |  | ||||||
|         super().__init__(obj, managed_uid, **kwargs) |  | ||||||
|         self.created_callback = created_callback |  | ||||||
|  |  | ||||||
|     def run(self): |  | ||||||
|         self._kwargs.setdefault("managed", self._managed_uid) |  | ||||||
|         obj, created = self._obj.objects.update_or_create( |  | ||||||
|             **{ |  | ||||||
|                 "managed": self._managed_uid, |  | ||||||
|                 "defaults": self._kwargs, |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|         if created and self.created_callback is not None: |  | ||||||
|             self.created_callback(obj) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ObjectManager: |  | ||||||
|     """Base class for Apps Object manager""" |  | ||||||
|  |  | ||||||
|     def run(self): |  | ||||||
|         """Main entrypoint for tasks, iterate through all implementation of this |  | ||||||
|         and execute all operations""" |  | ||||||
|         for sub in ObjectManager.__subclasses__(): |  | ||||||
|             sub_inst = sub() |  | ||||||
|             ops = sub_inst.reconcile() |  | ||||||
|             LOGGER.debug("Reconciling managed objects", manager=sub.__name__) |  | ||||||
|             for operation in ops: |  | ||||||
|                 operation.run() |  | ||||||
|  |  | ||||||
|     def reconcile(self) -> list[EnsureOp]: |  | ||||||
|         """Method which is implemented in subclass that returns a list of Operations""" |  | ||||||
|         raise NotImplementedError |  | ||||||
|  | |||||||
| @ -1,16 +1,37 @@ | |||||||
| # Generated by Django 4.0.6 on 2022-07-30 22:45 | # Generated by Django 4.0.6 on 2022-07-31 17:35 | ||||||
|  |  | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| import django.contrib.postgres.fields | import django.contrib.postgres.fields | ||||||
|  | from django.apps.registry import Apps | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
|  | from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||||
|  |     from authentik.blueprints.v1.tasks import blueprints_discover | ||||||
|  |  | ||||||
|  |     BlueprintInstance = apps.get_model("authentik_blueprints", "BlueprintInstance") | ||||||
|  |     Flow = apps.get_model("authentik_flows", "Flow") | ||||||
|  |  | ||||||
|  |     db_alias = schema_editor.connection.alias | ||||||
|  |     blueprints_discover() | ||||||
|  |     for blueprint in BlueprintInstance.objects.using(db_alias).all(): | ||||||
|  |         # If we already have flows (and we should always run before flow migrations) | ||||||
|  |         # then this is an existing install and we want to disable all blueprints | ||||||
|  |         if Flow.objects.using(db_alias).all().exists(): | ||||||
|  |             blueprint.enabled = False | ||||||
|  |         # System blueprints are always enabled | ||||||
|  |         if "/system/" in blueprint.path: | ||||||
|  |             blueprint.enabled = True | ||||||
|  |         blueprint.save() | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|     initial = True |     initial = True | ||||||
|  |  | ||||||
|     dependencies = [] |     dependencies = [("authentik_flows", "0001_initial")] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
| @ -38,6 +59,7 @@ class Migration(migrations.Migration): | |||||||
|                 ("path", models.TextField()), |                 ("path", models.TextField()), | ||||||
|                 ("context", models.JSONField()), |                 ("context", models.JSONField()), | ||||||
|                 ("last_applied", models.DateTimeField(auto_now=True)), |                 ("last_applied", models.DateTimeField(auto_now=True)), | ||||||
|  |                 ("last_applied_hash", models.TextField()), | ||||||
|                 ( |                 ( | ||||||
|                     "status", |                     "status", | ||||||
|                     models.TextField( |                     models.TextField( | ||||||
| @ -45,6 +67,7 @@ class Migration(migrations.Migration): | |||||||
|                             ("successful", "Successful"), |                             ("successful", "Successful"), | ||||||
|                             ("warning", "Warning"), |                             ("warning", "Warning"), | ||||||
|                             ("error", "Error"), |                             ("error", "Error"), | ||||||
|  |                             ("orphaned", "Orphaned"), | ||||||
|                             ("unknown", "Unknown"), |                             ("unknown", "Unknown"), | ||||||
|                         ] |                         ] | ||||||
|                     ), |                     ), | ||||||
| @ -63,4 +86,5 @@ class Migration(migrations.Migration): | |||||||
|                 "unique_together": {("name", "path")}, |                 "unique_together": {("name", "path")}, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|  |         migrations.RunPython(migration_blueprint_import), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -38,6 +38,7 @@ class BlueprintInstanceStatus(models.TextChoices): | |||||||
|     SUCCESSFUL = "successful" |     SUCCESSFUL = "successful" | ||||||
|     WARNING = "warning" |     WARNING = "warning" | ||||||
|     ERROR = "error" |     ERROR = "error" | ||||||
|  |     ORPHANED = "orphaned" | ||||||
|     UNKNOWN = "unknown" |     UNKNOWN = "unknown" | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -51,6 +52,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): | |||||||
|     path = models.TextField() |     path = models.TextField() | ||||||
|     context = models.JSONField() |     context = models.JSONField() | ||||||
|     last_applied = models.DateTimeField(auto_now=True) |     last_applied = models.DateTimeField(auto_now=True) | ||||||
|  |     last_applied_hash = models.TextField() | ||||||
|     status = models.TextField(choices=BlueprintInstanceStatus.choices) |     status = models.TextField(choices=BlueprintInstanceStatus.choices) | ||||||
|     enabled = models.BooleanField(default=True) |     enabled = models.BooleanField(default=True) | ||||||
|     managed_models = ArrayField(models.TextField()) |     managed_models = ArrayField(models.TextField()) | ||||||
|  | |||||||
| @ -1,17 +1,12 @@ | |||||||
| """managed Settings""" | """blueprint Settings""" | ||||||
| from celery.schedules import crontab | from celery.schedules import crontab | ||||||
|  |  | ||||||
| from authentik.lib.utils.time import fqdn_rand | from authentik.lib.utils.time import fqdn_rand | ||||||
|  |  | ||||||
| CELERY_BEAT_SCHEDULE = { | CELERY_BEAT_SCHEDULE = { | ||||||
|     "blueprints_reconcile": { |     "blueprints_v1_discover": { | ||||||
|         "task": "authentik.blueprints.tasks.managed_reconcile", |         "task": "authentik.blueprints.v1.tasks.blueprints_discover", | ||||||
|         "schedule": crontab(minute=fqdn_rand("managed_reconcile"), hour="*/4"), |         "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |  | ||||||
|     }, |  | ||||||
|     "blueprints_config_file_discovery": { |  | ||||||
|         "task": "authentik.blueprints.tasks.config_file_discovery", |  | ||||||
|         "schedule": crontab(minute=fqdn_rand("config_file_discovery"), hour="*"), |  | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -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)])) |  | ||||||
							
								
								
									
										30
									
								
								authentik/blueprints/tests/test_bundled.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								authentik/blueprints/tests/test_bundled.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | """test packaged blueprints""" | ||||||
|  | from glob import glob | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Callable | ||||||
|  |  | ||||||
|  | from django.test import TransactionTestCase | ||||||
|  | from django.utils.text import slugify | ||||||
|  |  | ||||||
|  | from authentik.blueprints.v1.importer import Importer | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestBundled(TransactionTestCase): | ||||||
|  |     """Empty class, test methods are added dynamically""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def blueprint_tester(file_name: str) -> Callable: | ||||||
|  |     """This is used instead of subTest for better visibility""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestBundled): | ||||||
|  |         with open(file_name, "r", encoding="utf8") as flow_yaml: | ||||||
|  |             importer = Importer(flow_yaml.read()) | ||||||
|  |         self.assertTrue(importer.validate()[0]) | ||||||
|  |         self.assertTrue(importer.apply()) | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for flow_file in glob("blueprints/**/*.yaml", recursive=True): | ||||||
|  |     method_name = slugify(Path(flow_file).stem).replace("-", "_").replace(".", "_") | ||||||
|  |     setattr(TestBundled, f"test_flow_{method_name}", blueprint_tester(flow_file)) | ||||||
| @ -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 |  | ||||||
| @ -37,14 +37,14 @@ class TestFlowTransport(TransactionTestCase): | |||||||
|     def test_bundle_invalid_format(self): |     def test_bundle_invalid_format(self): | ||||||
|         """Test bundle with invalid format""" |         """Test bundle with invalid format""" | ||||||
|         importer = Importer('{"version": 3}') |         importer = Importer('{"version": 3}') | ||||||
|         self.assertFalse(importer.validate()) |         self.assertFalse(importer.validate()[0]) | ||||||
|         importer = Importer( |         importer = Importer( | ||||||
|             ( |             ( | ||||||
|                 '{"version": 1,"entries":[{"identifiers":{},"attrs":{},' |                 '{"version": 1,"entries":[{"identifiers":{},"attrs":{},' | ||||||
|                 '"model": "authentik_core.User"}]}' |                 '"model": "authentik_core.User"}]}' | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         self.assertFalse(importer.validate()) |         self.assertFalse(importer.validate()[0]) | ||||||
|  |  | ||||||
|     def test_export_validate_import(self): |     def test_export_validate_import(self): | ||||||
|         """Test export and validate it""" |         """Test export and validate it""" | ||||||
| @ -70,7 +70,7 @@ class TestFlowTransport(TransactionTestCase): | |||||||
|             export_yaml = exporter.export_to_string() |             export_yaml = exporter.export_to_string() | ||||||
|  |  | ||||||
|         importer = Importer(export_yaml) |         importer = Importer(export_yaml) | ||||||
|         self.assertTrue(importer.validate()) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  |  | ||||||
|         self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) |         self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) | ||||||
| @ -80,7 +80,7 @@ class TestFlowTransport(TransactionTestCase): | |||||||
|         count_initial = Prompt.objects.filter(field_key="username").count() |         count_initial = Prompt.objects.filter(field_key="username").count() | ||||||
|  |  | ||||||
|         importer = Importer(STATIC_PROMPT_EXPORT) |         importer = Importer(STATIC_PROMPT_EXPORT) | ||||||
|         self.assertTrue(importer.validate()) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  |  | ||||||
|         count_before = Prompt.objects.filter(field_key="username").count() |         count_before = Prompt.objects.filter(field_key="username").count() | ||||||
| @ -116,7 +116,7 @@ class TestFlowTransport(TransactionTestCase): | |||||||
|             export_yaml = exporter.export_to_string() |             export_yaml = exporter.export_to_string() | ||||||
|  |  | ||||||
|         importer = Importer(export_yaml) |         importer = Importer(export_yaml) | ||||||
|         self.assertTrue(importer.validate()) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|         self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) |         self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) | ||||||
|         self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) |         self.assertTrue(Flow.objects.filter(slug=flow_slug).exists()) | ||||||
| @ -160,5 +160,5 @@ class TestFlowTransport(TransactionTestCase): | |||||||
|  |  | ||||||
|         importer = Importer(export_yaml) |         importer = Importer(export_yaml) | ||||||
|  |  | ||||||
|         self.assertTrue(importer.validate()) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  | |||||||
| @ -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)) |  | ||||||
| @ -13,6 +13,8 @@ from django.db.utils import IntegrityError | |||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.serializers import BaseSerializer, Serializer | from rest_framework.serializers import BaseSerializer, Serializer | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  | from structlog.testing import capture_logs | ||||||
|  | from structlog.types import EventDict | ||||||
| from yaml import load | from yaml import load | ||||||
|  |  | ||||||
| from authentik.blueprints.v1.common import ( | from authentik.blueprints.v1.common import ( | ||||||
| @ -198,17 +200,20 @@ class Importer: | |||||||
|             self.logger.debug("updated model", model=model, pk=model.pk) |             self.logger.debug("updated model", model=model, pk=model.pk) | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     def validate(self) -> bool: |     def validate(self) -> tuple[bool, list[EventDict]]: | ||||||
|         """Validate loaded flow export, ensure all models are allowed |         """Validate loaded blueprint export, ensure all models are allowed | ||||||
|         and serializers have no errors""" |         and serializers have no errors""" | ||||||
|         self.logger.debug("Starting flow import validation") |         self.logger.debug("Starting blueprint import validation") | ||||||
|         orig_import = deepcopy(self.__import) |         orig_import = deepcopy(self.__import) | ||||||
|         if self.__import.version != 1: |         if self.__import.version != 1: | ||||||
|             self.logger.warning("Invalid bundle version") |             self.logger.warning("Invalid bundle version") | ||||||
|             return False |             return False, [] | ||||||
|         with transaction_rollback(): |         with ( | ||||||
|  |             transaction_rollback(), | ||||||
|  |             capture_logs() as logs, | ||||||
|  |         ): | ||||||
|             successful = self._apply_models() |             successful = self._apply_models() | ||||||
|             if not successful: |             if not successful: | ||||||
|                 self.logger.debug("Flow validation failed") |                 self.logger.debug("blueprint validation failed") | ||||||
|         self.__import = orig_import |         self.__import = orig_import | ||||||
|         return successful |         return successful, logs | ||||||
|  | |||||||
							
								
								
									
										84
									
								
								authentik/blueprints/v1/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								authentik/blueprints/v1/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | """v1 blueprints tasks""" | ||||||
|  | from glob import glob | ||||||
|  | from hashlib import sha512 | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from django.db import DatabaseError, InternalError, ProgrammingError | ||||||
|  | from yaml import load | ||||||
|  |  | ||||||
|  | from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus | ||||||
|  | from authentik.blueprints.v1.common import BlueprintLoader | ||||||
|  | from authentik.blueprints.v1.importer import Importer | ||||||
|  | from authentik.events.monitored_tasks import ( | ||||||
|  |     MonitoredTask, | ||||||
|  |     TaskResult, | ||||||
|  |     TaskResultStatus, | ||||||
|  |     prefill_task, | ||||||
|  | ) | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task() | ||||||
|  | @prefill_task | ||||||
|  | def blueprints_discover(): | ||||||
|  |     """Find blueprints and check if they need to be created in the database""" | ||||||
|  |     for folder in CONFIG.y("blueprint_locations"): | ||||||
|  |         for file in glob(f"{folder}/**/*.yaml", recursive=True): | ||||||
|  |             check_blueprint_v1_file(Path(file)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def check_blueprint_v1_file(path: Path): | ||||||
|  |     """Check if blueprint should be imported""" | ||||||
|  |     with open(path, "r", encoding="utf-8") as blueprint_file: | ||||||
|  |         raw_blueprint = load(blueprint_file.read(), BlueprintLoader) | ||||||
|  |         version = raw_blueprint.get("version", 1) | ||||||
|  |         if version != 1: | ||||||
|  |             return | ||||||
|  |         blueprint_file.seek(0) | ||||||
|  |     file_hash = sha512(path.read_bytes()).hexdigest() | ||||||
|  |     instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first() | ||||||
|  |     if not instance: | ||||||
|  |         instance = BlueprintInstance( | ||||||
|  |             name=path.name, | ||||||
|  |             path=str(path), | ||||||
|  |             context={}, | ||||||
|  |             status=BlueprintInstanceStatus.UNKNOWN, | ||||||
|  |             enabled=True, | ||||||
|  |             managed_models=[], | ||||||
|  |         ) | ||||||
|  |         instance.save() | ||||||
|  |     if instance.last_applied_hash != file_hash: | ||||||
|  |         apply_blueprint.delay(instance.pk.hex) | ||||||
|  |         instance.last_applied_hash = file_hash | ||||||
|  |         instance.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task( | ||||||
|  |     bind=True, | ||||||
|  |     base=MonitoredTask, | ||||||
|  | ) | ||||||
|  | def apply_blueprint(self: MonitoredTask, instance_pk: str): | ||||||
|  |     """Apply single blueprint""" | ||||||
|  |     self.save_on_success = False | ||||||
|  |     try: | ||||||
|  |         instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first() | ||||||
|  |         if not instance: | ||||||
|  |             return | ||||||
|  |         with open(instance.path, "r", encoding="utf-8") as blueprint_file: | ||||||
|  |             importer = Importer(blueprint_file.read()) | ||||||
|  |             valid, logs = importer.validate() | ||||||
|  |             if not valid: | ||||||
|  |                 instance.status = BlueprintInstanceStatus.ERROR | ||||||
|  |                 instance.save() | ||||||
|  |                 self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs])) | ||||||
|  |                 return | ||||||
|  |             applied = importer.apply() | ||||||
|  |             if not applied: | ||||||
|  |                 instance.status = BlueprintInstanceStatus.ERROR | ||||||
|  |                 instance.save() | ||||||
|  |                 self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply")) | ||||||
|  |     except (DatabaseError, ProgrammingError, InternalError) as exc: | ||||||
|  |         instance.status = BlueprintInstanceStatus.ERROR | ||||||
|  |         instance.save() | ||||||
|  |         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||||
| @ -1,22 +1,37 @@ | |||||||
| """authentik core app config""" | """authentik core app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| class AuthentikCoreConfig(AppConfig): |  | ||||||
|  | class AuthentikCoreConfig(ManagedAppConfig): | ||||||
|     """authentik core app config""" |     """authentik core app config""" | ||||||
|  |  | ||||||
|     name = "authentik.core" |     name = "authentik.core" | ||||||
|     label = "authentik_core" |     label = "authentik_core" | ||||||
|     verbose_name = "authentik Core" |     verbose_name = "authentik Core" | ||||||
|     mountpoint = "" |     mountpoint = "" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_core_signals(self): | ||||||
|         import_module("authentik.core.signals") |         """Load core signals""" | ||||||
|         import_module("authentik.core.managed") |         self.import_module("authentik.core.signals") | ||||||
|  |  | ||||||
|  |     def reconcile_debug_worker_hook(self): | ||||||
|  |         """Dispatch startup tasks inline when debugging""" | ||||||
|         if settings.DEBUG: |         if settings.DEBUG: | ||||||
|             from authentik.root.celery import worker_ready_hook |             from authentik.root.celery import worker_ready_hook | ||||||
|  |  | ||||||
|             worker_ready_hook() |             worker_ready_hook() | ||||||
|  |  | ||||||
|  |     def reconcile_source_inbuilt(self): | ||||||
|  |         """Reconcile inbuilt source""" | ||||||
|  |         from authentik.core.models import Source | ||||||
|  |  | ||||||
|  |         Source.objects.update_or_create( | ||||||
|  |             defaults={ | ||||||
|  |                 "name": "authentik Built-in", | ||||||
|  |                 "slug": "authentik-built-in", | ||||||
|  |             }, | ||||||
|  |             managed="goauthentik.io/sources/inbuilt", | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -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", |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -22,8 +22,8 @@ from structlog.stdlib import get_logger | |||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.crypto.apps import MANAGED_KEY | ||||||
| from authentik.crypto.builder import CertificateBuilder | from authentik.crypto.builder import CertificateBuilder | ||||||
| from authentik.crypto.managed import MANAGED_KEY |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,16 +1,55 @@ | |||||||
| """authentik crypto app config""" | """authentik crypto app config""" | ||||||
| from importlib import import_module | from datetime import datetime | ||||||
|  | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from authentik.crypto.models import CertificateKeyPair | ||||||
|  |  | ||||||
|  | MANAGED_KEY = "goauthentik.io/crypto/jwt-managed" | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikCryptoConfig(AppConfig): | class AuthentikCryptoConfig(ManagedAppConfig): | ||||||
|     """authentik crypto app config""" |     """authentik crypto app config""" | ||||||
|  |  | ||||||
|     name = "authentik.crypto" |     name = "authentik.crypto" | ||||||
|     label = "authentik_crypto" |     label = "authentik_crypto" | ||||||
|     verbose_name = "authentik Crypto" |     verbose_name = "authentik Crypto" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_crypto_tasks(self): | ||||||
|         import_module("authentik.crypto.managed") |         """Load crypto tasks""" | ||||||
|         import_module("authentik.crypto.tasks") |         self.import_module("authentik.crypto.tasks") | ||||||
|  |  | ||||||
|  |     def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None): | ||||||
|  |         from authentik.crypto.builder import CertificateBuilder | ||||||
|  |         from authentik.crypto.models import CertificateKeyPair | ||||||
|  |  | ||||||
|  |         builder = CertificateBuilder() | ||||||
|  |         builder.common_name = "goauthentik.io" | ||||||
|  |         builder.build( | ||||||
|  |             subject_alt_names=["goauthentik.io"], | ||||||
|  |             validity_days=360, | ||||||
|  |         ) | ||||||
|  |         if not cert: | ||||||
|  |  | ||||||
|  |             cert = CertificateKeyPair() | ||||||
|  |         cert.certificate_data = builder.certificate | ||||||
|  |         cert.key_data = builder.private_key | ||||||
|  |         cert.name = "authentik Internal JWT Certificate" | ||||||
|  |         cert.managed = MANAGED_KEY | ||||||
|  |         cert.save() | ||||||
|  |  | ||||||
|  |     def reconcile_managed_jwt_cert(self): | ||||||
|  |         """Ensure managed JWT certificate""" | ||||||
|  |         from authentik.crypto.models import CertificateKeyPair | ||||||
|  |  | ||||||
|  |         certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY) | ||||||
|  |         if not certs.exists(): | ||||||
|  |             self._create_update_cert() | ||||||
|  |             return | ||||||
|  |         cert: CertificateKeyPair = certs.first() | ||||||
|  |         now = datetime.now() | ||||||
|  |         if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after: | ||||||
|  |             self._create_update_cert(cert) | ||||||
|  | |||||||
| @ -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 [] |  | ||||||
| @ -1,9 +1,8 @@ | |||||||
| """authentik events app""" | """authentik events app""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from prometheus_client import Gauge | from prometheus_client import Gauge | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| GAUGE_TASKS = Gauge( | GAUGE_TASKS = Gauge( | ||||||
|     "authentik_system_tasks", |     "authentik_system_tasks", | ||||||
|     "System tasks and their status", |     "System tasks and their status", | ||||||
| @ -11,12 +10,14 @@ GAUGE_TASKS = Gauge( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikEventsConfig(AppConfig): | class AuthentikEventsConfig(ManagedAppConfig): | ||||||
|     """authentik events app""" |     """authentik events app""" | ||||||
|  |  | ||||||
|     name = "authentik.events" |     name = "authentik.events" | ||||||
|     label = "authentik_events" |     label = "authentik_events" | ||||||
|     verbose_name = "authentik Events" |     verbose_name = "authentik Events" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_events_signals(self): | ||||||
|         import_module("authentik.events.signals") |         """Load events signals""" | ||||||
|  |         self.import_module("authentik.events.signals") | ||||||
|  | |||||||
| @ -168,7 +168,8 @@ class FlowViewSet(UsedByMixin, ModelViewSet): | |||||||
|         if not file: |         if not file: | ||||||
|             return HttpResponseBadRequest() |             return HttpResponseBadRequest() | ||||||
|         importer = Importer(file.read().decode()) |         importer = Importer(file.read().decode()) | ||||||
|         valid = importer.validate() |         valid, _logs = importer.validate() | ||||||
|  |         # TODO: return logs | ||||||
|         if not valid: |         if not valid: | ||||||
|             return HttpResponseBadRequest() |             return HttpResponseBadRequest() | ||||||
|         successful = importer.apply() |         successful = importer.apply() | ||||||
|  | |||||||
| @ -1,10 +1,7 @@ | |||||||
| """authentik flows app config""" | """authentik flows app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from django.db.utils import ProgrammingError |  | ||||||
| from prometheus_client import Gauge, Histogram | from prometheus_client import Gauge, Histogram | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
| from authentik.lib.utils.reflection import all_subclasses | from authentik.lib.utils.reflection import all_subclasses | ||||||
|  |  | ||||||
| GAUGE_FLOWS_CACHED = Gauge( | GAUGE_FLOWS_CACHED = Gauge( | ||||||
| @ -18,20 +15,22 @@ HIST_FLOWS_PLAN_TIME = Histogram( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikFlowsConfig(AppConfig): | class AuthentikFlowsConfig(ManagedAppConfig): | ||||||
|     """authentik flows app config""" |     """authentik flows app config""" | ||||||
|  |  | ||||||
|     name = "authentik.flows" |     name = "authentik.flows" | ||||||
|     label = "authentik_flows" |     label = "authentik_flows" | ||||||
|     mountpoint = "flows/" |     mountpoint = "flows/" | ||||||
|     verbose_name = "authentik Flows" |     verbose_name = "authentik Flows" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_flows_signals(self): | ||||||
|         import_module("authentik.flows.signals") |         """Load flows signals""" | ||||||
|         try: |         self.import_module("authentik.flows.signals") | ||||||
|             from authentik.flows.models import Stage |  | ||||||
|  |  | ||||||
|             for stage in all_subclasses(Stage): |     def reconcile_stages_loaded(self): | ||||||
|                 _ = stage().type |         """Ensure all stages are loaded""" | ||||||
|         except ProgrammingError: |         from authentik.flows.models import Stage | ||||||
|             pass |  | ||||||
|  |         for stage in all_subclasses(Stage): | ||||||
|  |             _ = stage().type | ||||||
|  | |||||||
| @ -62,7 +62,6 @@ ldap: | |||||||
|   tls: |   tls: | ||||||
|     ciphers: null |     ciphers: null | ||||||
|  |  | ||||||
| config_file_dir: "/config" |  | ||||||
| cookie_domain: null | cookie_domain: null | ||||||
| disable_update_check: false | disable_update_check: false | ||||||
| disable_startup_analytics: false | disable_startup_analytics: false | ||||||
| @ -79,3 +78,6 @@ gdpr_compliance: true | |||||||
| cert_discovery_dir: /certs | cert_discovery_dir: /certs | ||||||
| default_token_length: 128 | default_token_length: 128 | ||||||
| impersonation: true | impersonation: true | ||||||
|  |  | ||||||
|  | blueprint_locations: | ||||||
|  |   - /blueprints | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin | |||||||
| from authentik.core.api.utils import PassiveSerializer, is_dict | from authentik.core.api.utils import PassiveSerializer, is_dict | ||||||
| from authentik.core.models import Provider | from authentik.core.models import Provider | ||||||
| from authentik.outposts.api.service_connections import ServiceConnectionSerializer | from authentik.outposts.api.service_connections import ServiceConnectionSerializer | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.models import Outpost, OutpostConfig, OutpostType, default_outpost_config | from authentik.outposts.models import Outpost, OutpostConfig, OutpostType, default_outpost_config | ||||||
| from authentik.providers.ldap.models import LDAPProvider | from authentik.providers.ldap.models import LDAPProvider | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
|  | |||||||
| @ -1,10 +1,9 @@ | |||||||
| """authentik outposts app config""" | """authentik outposts app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from prometheus_client import Gauge | from prometheus_client import Gauge | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| GAUGE_OUTPOSTS_CONNECTED = Gauge( | GAUGE_OUTPOSTS_CONNECTED = Gauge( | ||||||
| @ -15,15 +14,47 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( | |||||||
|     "Last update from any outpost", |     "Last update from any outpost", | ||||||
|     ["outpost", "uid", "version"], |     ["outpost", "uid", "version"], | ||||||
| ) | ) | ||||||
|  | MANAGED_OUTPOST = "goauthentik.io/outposts/embedded" | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikOutpostConfig(AppConfig): | class AuthentikOutpostConfig(ManagedAppConfig): | ||||||
|     """authentik outposts app config""" |     """authentik outposts app config""" | ||||||
|  |  | ||||||
|     name = "authentik.outposts" |     name = "authentik.outposts" | ||||||
|     label = "authentik_outposts" |     label = "authentik_outposts" | ||||||
|     verbose_name = "authentik Outpost" |     verbose_name = "authentik Outpost" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_outposts_signals(self): | ||||||
|         import_module("authentik.outposts.signals") |         """Load outposts signals""" | ||||||
|         import_module("authentik.outposts.managed") |         self.import_module("authentik.outposts.signals") | ||||||
|  |  | ||||||
|  |     def reconcile_embedded_outpost(self): | ||||||
|  |         """Ensure embedded outpost""" | ||||||
|  |         from authentik.outposts.models import ( | ||||||
|  |             DockerServiceConnection, | ||||||
|  |             KubernetesServiceConnection, | ||||||
|  |             Outpost, | ||||||
|  |             OutpostConfig, | ||||||
|  |             OutpostType, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         outpost, updated = Outpost.objects.update_or_create( | ||||||
|  |             defaults={ | ||||||
|  |                 "name": "authentik Embedded Outpost", | ||||||
|  |                 "type": OutpostType.PROXY, | ||||||
|  |             }, | ||||||
|  |             managed=MANAGED_OUTPOST, | ||||||
|  |         ) | ||||||
|  |         if updated: | ||||||
|  |             if KubernetesServiceConnection.objects.exists(): | ||||||
|  |                 outpost.service_connection = KubernetesServiceConnection.objects.first() | ||||||
|  |             elif DockerServiceConnection.objects.exists(): | ||||||
|  |                 outpost.service_connection = DockerServiceConnection.objects.first() | ||||||
|  |             outpost.config = OutpostConfig( | ||||||
|  |                 kubernetes_disabled_components=[ | ||||||
|  |                     "deployment", | ||||||
|  |                     "secret", | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             outpost.save() | ||||||
|  | |||||||
| @ -14,10 +14,10 @@ from structlog.stdlib import get_logger | |||||||
| from yaml import safe_dump | from yaml import safe_dump | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
|  | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException | from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException | ||||||
| from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException | from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException | ||||||
| from authentik.outposts.docker_tls import DockerInlineTLS | from authentik.outposts.docker_tls import DockerInlineTLS | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST |  | ||||||
| from authentik.outposts.models import ( | from authentik.outposts.models import ( | ||||||
|     DockerServiceConnection, |     DockerServiceConnection, | ||||||
|     Outpost, |     Outpost, | ||||||
|  | |||||||
| @ -10,8 +10,8 @@ from structlog.stdlib import get_logger | |||||||
| from urllib3.exceptions import HTTPError | from urllib3.exceptions import HTTPError | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
|  | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate | from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from authentik.outposts.controllers.kubernetes import KubernetesController |     from authentik.outposts.controllers.kubernetes import KubernetesController | ||||||
|  | |||||||
| @ -78,7 +78,7 @@ class DockerInlineSSH: | |||||||
|         """Cleanup when we're done""" |         """Cleanup when we're done""" | ||||||
|         try: |         try: | ||||||
|             os.unlink(self.key_path) |             os.unlink(self.key_path) | ||||||
|             with open(self.config_path, "r+", encoding="utf-8") as ssh_config: |             with open(self.config_path, "r", encoding="utf-8") as ssh_config: | ||||||
|                 start = 0 |                 start = 0 | ||||||
|                 end = 0 |                 end = 0 | ||||||
|                 lines = ssh_config.readlines() |                 lines = ssh_config.readlines() | ||||||
|  | |||||||
| @ -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, |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -233,7 +233,7 @@ def _outpost_single_update(outpost: Outpost, layer=None): | |||||||
| def outpost_local_connection(): | def outpost_local_connection(): | ||||||
|     """Checks the local environment and create Service connections.""" |     """Checks the local environment and create Service connections.""" | ||||||
|     if not CONFIG.y_bool("outposts.discover"): |     if not CONFIG.y_bool("outposts.discover"): | ||||||
|         LOGGER.debug("outpost integration discovery is disabled") |         LOGGER.debug("Outpost integration discovery is disabled") | ||||||
|         return |         return | ||||||
|     # Explicitly check against token filename, as that's |     # Explicitly check against token filename, as that's | ||||||
|     # only present when the integration is enabled |     # only present when the integration is enabled | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| """Docker controller tests""" | """Docker controller tests""" | ||||||
|  | from django.apps import apps | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from docker.models.containers import Container | from docker.models.containers import Container | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.controllers.base import ControllerException | from authentik.outposts.controllers.base import ControllerException | ||||||
| from authentik.outposts.controllers.docker import DockerController | from authentik.outposts.controllers.docker import DockerController | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST |  | ||||||
| from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType | from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType | ||||||
| from authentik.providers.proxy.controllers.docker import ProxyDockerController | from authentik.providers.proxy.controllers.docker import ProxyDockerController | ||||||
|  |  | ||||||
| @ -19,7 +19,7 @@ class DockerControllerTests(TestCase): | |||||||
|             type=OutpostType.PROXY, |             type=OutpostType.PROXY, | ||||||
|         ) |         ) | ||||||
|         self.integration = DockerServiceConnection(name="test") |         self.integration = DockerServiceConnection(name="test") | ||||||
|         ObjectManager().run() |         apps.get_app_config("authentik_outposts").reconcile() | ||||||
|  |  | ||||||
|     def test_init_managed(self): |     def test_init_managed(self): | ||||||
|         """Docker controller shouldn't do anything for managed outpost""" |         """Docker controller shouldn't do anything for managed outpost""" | ||||||
|  | |||||||
| @ -1,9 +1,8 @@ | |||||||
| """authentik policies app config""" | """authentik policies app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from prometheus_client import Gauge, Histogram | from prometheus_client import Gauge, Histogram | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| GAUGE_POLICIES_CACHED = Gauge( | GAUGE_POLICIES_CACHED = Gauge( | ||||||
|     "authentik_policies_cached", |     "authentik_policies_cached", | ||||||
|     "Cached Policies", |     "Cached Policies", | ||||||
| @ -27,12 +26,14 @@ HIST_POLICIES_EXECUTION_TIME = Histogram( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikPoliciesConfig(AppConfig): | class AuthentikPoliciesConfig(ManagedAppConfig): | ||||||
|     """authentik policies app config""" |     """authentik policies app config""" | ||||||
|  |  | ||||||
|     name = "authentik.policies" |     name = "authentik.policies" | ||||||
|     label = "authentik_policies" |     label = "authentik_policies" | ||||||
|     verbose_name = "authentik Policies" |     verbose_name = "authentik Policies" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_policies_signals(self): | ||||||
|         import_module("authentik.policies.signals") |         """Load policies signals""" | ||||||
|  |         self.import_module("authentik.policies.signals") | ||||||
|  | |||||||
| @ -1,16 +1,19 @@ | |||||||
| """Authentik reputation_policy app config""" | """Authentik reputation_policy app config""" | ||||||
| from importlib import import_module | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikPolicyReputationConfig(AppConfig): | class AuthentikPolicyReputationConfig(ManagedAppConfig): | ||||||
|     """Authentik reputation app config""" |     """Authentik reputation app config""" | ||||||
|  |  | ||||||
|     name = "authentik.policies.reputation" |     name = "authentik.policies.reputation" | ||||||
|     label = "authentik_policies_reputation" |     label = "authentik_policies_reputation" | ||||||
|     verbose_name = "authentik Policies.Reputation" |     verbose_name = "authentik Policies.Reputation" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_policies_reputation_signals(self): | ||||||
|         import_module("authentik.policies.reputation.signals") |         """Load policies.reputation signals""" | ||||||
|         import_module("authentik.policies.reputation.tasks") |         self.import_module("authentik.policies.reputation.signals") | ||||||
|  |  | ||||||
|  |     def reconcile_load_policies_reputation_tasks(self): | ||||||
|  |         """Load policies.reputation tasks""" | ||||||
|  |         self.import_module("authentik.policies.reputation.tasks") | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| """authentik oauth provider app config""" | """authentik oauth provider app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -14,6 +12,3 @@ class AuthentikProviderOAuth2Config(AppConfig): | |||||||
|         "authentik.providers.oauth2.urls_github": "", |         "authentik.providers.oauth2.urls_github": "", | ||||||
|         "authentik.providers.oauth2.urls": "application/o/", |         "authentik.providers.oauth2.urls": "application/o/", | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     def ready(self) -> None: |  | ||||||
|         import_module("authentik.providers.oauth2.managed") |  | ||||||
|  | |||||||
| @ -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, |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -5,7 +5,7 @@ from django.test import RequestFactory | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from jwt import decode | from jwt import decode | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents | from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| @ -24,9 +24,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase | |||||||
| class TestTokenClientCredentials(OAuthTestCase): | class TestTokenClientCredentials(OAuthTestCase): | ||||||
|     """Test token (client_credentials) view""" |     """Test token (client_credentials) view""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         ObjectManager().run() |  | ||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from django.test import RequestFactory | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from jwt import decode | from jwt import decode | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application, Group | from authentik.core.models import Application, Group | ||||||
| from authentik.core.tests.utils import create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| @ -26,9 +26,9 @@ from authentik.sources.oauth.models import OAuthSource | |||||||
| class TestTokenClientCredentialsJWTSource(OAuthTestCase): | class TestTokenClientCredentialsJWTSource(OAuthTestCase): | ||||||
|     """Test token (client_credentials, with JWT) view""" |     """Test token (client_credentials, with JWT) view""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         ObjectManager().run() |  | ||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|         self.cert = create_test_cert() |         self.cert = create_test_cert() | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from dataclasses import asdict | |||||||
|  |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| @ -16,9 +16,9 @@ from authentik.providers.oauth2.tests.utils import OAuthTestCase | |||||||
| class TestUserinfo(OAuthTestCase): | class TestUserinfo(OAuthTestCase): | ||||||
|     """Test token view""" |     """Test token view""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         ObjectManager().run() |  | ||||||
|         self.app = Application.objects.create(name=generate_id(), slug=generate_id()) |         self.app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| """authentik Proxy app""" | """authentik Proxy app""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -10,6 +8,3 @@ class AuthentikProviderProxyConfig(AppConfig): | |||||||
|     name = "authentik.providers.proxy" |     name = "authentik.providers.proxy" | ||||||
|     label = "authentik_providers_proxy" |     label = "authentik_providers_proxy" | ||||||
|     verbose_name = "authentik Providers.Proxy" |     verbose_name = "authentik Providers.Proxy" | ||||||
|  |  | ||||||
|     def ready(self) -> None: |  | ||||||
|         import_module("authentik.providers.proxy.managed") |  | ||||||
|  | |||||||
| @ -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, |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -1,5 +1,4 @@ | |||||||
| """authentik SAML IdP app config""" | """authentik SAML IdP app config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
| @ -11,6 +10,3 @@ class AuthentikProviderSAMLConfig(AppConfig): | |||||||
|     label = "authentik_providers_saml" |     label = "authentik_providers_saml" | ||||||
|     verbose_name = "authentik Providers.SAML" |     verbose_name = "authentik Providers.SAML" | ||||||
|     mountpoint = "application/saml/" |     mountpoint = "application/saml/" | ||||||
|  |  | ||||||
|     def ready(self) -> None: |  | ||||||
|         import_module("authentik.providers.saml.managed") |  | ||||||
|  | |||||||
| @ -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="", |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -4,7 +4,7 @@ from base64 import b64encode | |||||||
| from django.http.request import QueryDict | from django.http.request import QueryDict | ||||||
| from django.test import RequestFactory, TestCase | from django.test import RequestFactory, TestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| @ -74,8 +74,8 @@ qNAZMq1DqpibfCBg | |||||||
| class TestAuthNRequest(TestCase): | class TestAuthNRequest(TestCase): | ||||||
|     """Test AuthN Request generator and parser""" |     """Test AuthN Request generator and parser""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/providers-saml.yaml") | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         ObjectManager().run() |  | ||||||
|         cert = create_test_cert() |         cert = create_test_cert() | ||||||
|         self.provider: SAMLProvider = SAMLProvider.objects.create( |         self.provider: SAMLProvider = SAMLProvider.objects.create( | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from base64 import b64encode | |||||||
| from django.test import RequestFactory, TestCase | from django.test import RequestFactory, TestCase | ||||||
| from lxml import etree  # nosec | from lxml import etree  # nosec | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.tests.utils import create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_cert, create_test_flow | ||||||
| from authentik.lib.tests.utils import get_request | from authentik.lib.tests.utils import get_request | ||||||
| from authentik.lib.xml import lxml_from_string | from authentik.lib.xml import lxml_from_string | ||||||
| @ -18,8 +18,8 @@ from authentik.sources.saml.processors.request import RequestProcessor | |||||||
| class TestSchema(TestCase): | class TestSchema(TestCase): | ||||||
|     """Test Requests and Responses against schema""" |     """Test Requests and Responses against schema""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/providers-saml.yaml") | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         ObjectManager().run() |  | ||||||
|         cert = create_test_cert() |         cert = create_test_cert() | ||||||
|         self.provider: SAMLProvider = SAMLProvider.objects.create( |         self.provider: SAMLProvider = SAMLProvider.objects.create( | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|  | |||||||
| @ -58,6 +58,7 @@ def task_prerun_hook(task_id: str, task, *args, **kwargs): | |||||||
| @task_postrun.connect | @task_postrun.connect | ||||||
| def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs): | def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs): | ||||||
|     """Log task_id on worker""" |     """Log task_id on worker""" | ||||||
|  |     CTX_TASK_ID.set(...) | ||||||
|     LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state) |     LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state) | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -69,6 +70,7 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs): | |||||||
|     from authentik.events.models import Event, EventAction |     from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|     LOGGER.warning("Task failure", exc=exception) |     LOGGER.warning("Task failure", exc=exception) | ||||||
|  |     CTX_TASK_ID.set(...) | ||||||
|     if before_send({}, {"exc_info": (None, exception, None)}) is not None: |     if before_send({}, {"exc_info": (None, exception, None)}) is not None: | ||||||
|         Event.new(EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception)).save() |         Event.new(EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception)).save() | ||||||
|  |  | ||||||
| @ -76,7 +78,6 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs): | |||||||
| def _get_startup_tasks() -> list[Callable]: | def _get_startup_tasks() -> list[Callable]: | ||||||
|     """Get all tasks to be run on startup""" |     """Get all tasks to be run on startup""" | ||||||
|     from authentik.admin.tasks import clear_update_notifications |     from authentik.admin.tasks import clear_update_notifications | ||||||
|     from authentik.blueprints.tasks import managed_reconcile |  | ||||||
|     from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection |     from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection | ||||||
|     from authentik.providers.proxy.tasks import proxy_set_defaults |     from authentik.providers.proxy.tasks import proxy_set_defaults | ||||||
|  |  | ||||||
| @ -85,7 +86,6 @@ def _get_startup_tasks() -> list[Callable]: | |||||||
|         outpost_local_connection, |         outpost_local_connection, | ||||||
|         outpost_controller_all, |         outpost_controller_all, | ||||||
|         proxy_set_defaults, |         proxy_set_defaults, | ||||||
|         managed_reconcile, |  | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,16 +1,15 @@ | |||||||
| """authentik ldap source config""" | """authentik ldap source config""" | ||||||
| from importlib import import_module | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikSourceLDAPConfig(AppConfig): | class AuthentikSourceLDAPConfig(ManagedAppConfig): | ||||||
|     """Authentik ldap app config""" |     """Authentik ldap app config""" | ||||||
|  |  | ||||||
|     name = "authentik.sources.ldap" |     name = "authentik.sources.ldap" | ||||||
|     label = "authentik_sources_ldap" |     label = "authentik_sources_ldap" | ||||||
|     verbose_name = "authentik Sources.LDAP" |     verbose_name = "authentik Sources.LDAP" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_sources_ldap_signals(self): | ||||||
|         import_module("authentik.sources.ldap.signals") |         """Load sources.ldap signals""" | ||||||
|         import_module("authentik.sources.ldap.managed") |         self.import_module("authentik.sources.ldap.signals") | ||||||
|  | |||||||
| @ -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')", |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
| @ -4,7 +4,7 @@ from unittest.mock import Mock, PropertyMock, patch | |||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.lib.generators import generate_key | from authentik.lib.generators import generate_key | ||||||
| from authentik.sources.ldap.auth import LDAPBackend | from authentik.sources.ldap.auth import LDAPBackend | ||||||
| @ -19,8 +19,8 @@ LDAP_PASSWORD = generate_key() | |||||||
| class LDAPSyncTests(TestCase): | class LDAPSyncTests(TestCase): | ||||||
|     """LDAP Sync tests""" |     """LDAP Sync tests""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/sources-ldap.yaml") | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         ObjectManager().run() |  | ||||||
|         self.source = LDAPSource.objects.create( |         self.source = LDAPSource.objects.create( | ||||||
|             name="ldap", |             name="ldap", | ||||||
|             slug="ldap", |             slug="ldap", | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch | |||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| @ -23,8 +23,8 @@ LDAP_PASSWORD = generate_key() | |||||||
| class LDAPSyncTests(TestCase): | class LDAPSyncTests(TestCase): | ||||||
|     """LDAP Sync tests""" |     """LDAP Sync tests""" | ||||||
|  |  | ||||||
|  |     @apply_blueprint("blueprints/system/sources-ldap.yaml") | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         ObjectManager().run() |  | ||||||
|         self.source: LDAPSource = LDAPSource.objects.create( |         self.source: LDAPSource = LDAPSource.objects.create( | ||||||
|             name="ldap", |             name="ldap", | ||||||
|             slug="ldap", |             slug="ldap", | ||||||
|  | |||||||
| @ -1,9 +1,8 @@ | |||||||
| """authentik oauth_client config""" | """authentik oauth_client config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| AUTHENTIK_SOURCES_OAUTH_TYPES = [ | AUTHENTIK_SOURCES_OAUTH_TYPES = [ | ||||||
| @ -21,18 +20,19 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikSourceOAuthConfig(AppConfig): | class AuthentikSourceOAuthConfig(ManagedAppConfig): | ||||||
|     """authentik source.oauth config""" |     """authentik source.oauth config""" | ||||||
|  |  | ||||||
|     name = "authentik.sources.oauth" |     name = "authentik.sources.oauth" | ||||||
|     label = "authentik_sources_oauth" |     label = "authentik_sources_oauth" | ||||||
|     verbose_name = "authentik Sources.OAuth" |     verbose_name = "authentik Sources.OAuth" | ||||||
|     mountpoint = "source/oauth/" |     mountpoint = "source/oauth/" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_sources_loaded(self): | ||||||
|         """Load source_types from config file""" |         """Load source_types from config file""" | ||||||
|         for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES: |         for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES: | ||||||
|             try: |             try: | ||||||
|                 import_module(source_type) |                 self.import_module(source_type) | ||||||
|             except ImportError as exc: |             except ImportError as exc: | ||||||
|                 LOGGER.warning("Failed to load OAuth Source", exc=exc) |                 LOGGER.warning("Failed to load OAuth Source", exc=exc) | ||||||
|  | |||||||
| @ -1,17 +1,16 @@ | |||||||
| """Authentik SAML app config""" | """Authentik SAML app config""" | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikSourceSAMLConfig(AppConfig): | class AuthentikSourceSAMLConfig(ManagedAppConfig): | ||||||
|     """authentik saml source app config""" |     """authentik saml source app config""" | ||||||
|  |  | ||||||
|     name = "authentik.sources.saml" |     name = "authentik.sources.saml" | ||||||
|     label = "authentik_sources_saml" |     label = "authentik_sources_saml" | ||||||
|     verbose_name = "authentik Sources.SAML" |     verbose_name = "authentik Sources.SAML" | ||||||
|     mountpoint = "source/saml/" |     mountpoint = "source/saml/" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_sources_saml_signals(self): | ||||||
|         import_module("authentik.sources.saml.signals") |         """Load sources.saml signals""" | ||||||
|  |         self.import_module("authentik.sources.saml.signals") | ||||||
|  | |||||||
| @ -1,15 +1,15 @@ | |||||||
| """Authenticator Static stage""" | """Authenticator Static stage""" | ||||||
| from importlib import import_module | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikStageAuthenticatorStaticConfig(AppConfig): | class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig): | ||||||
|     """Authenticator Static stage""" |     """Authenticator Static stage""" | ||||||
|  |  | ||||||
|     name = "authentik.stages.authenticator_static" |     name = "authentik.stages.authenticator_static" | ||||||
|     label = "authentik_stages_authenticator_static" |     label = "authentik_stages_authenticator_static" | ||||||
|     verbose_name = "authentik Stages.Authenticator.Static" |     verbose_name = "authentik Stages.Authenticator.Static" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_stages_authenticator_static_signals(self): | ||||||
|         import_module("authentik.stages.authenticator_static.signals") |         """Load stages.authenticator_static signals""" | ||||||
|  |         self.import_module("authentik.stages.authenticator_static.signals") | ||||||
|  | |||||||
| @ -1,30 +1,26 @@ | |||||||
| """authentik email stage config""" | """authentik email stage config""" | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.apps import AppConfig |  | ||||||
| from django.db import ProgrammingError |  | ||||||
| from django.template.exceptions import TemplateDoesNotExist | from django.template.exceptions import TemplateDoesNotExist | ||||||
| from django.template.loader import get_template | from django.template.loader import get_template | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.blueprints.manager import ManagedAppConfig | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikStageEmailConfig(AppConfig): | class AuthentikStageEmailConfig(ManagedAppConfig): | ||||||
|     """authentik email stage config""" |     """authentik email stage config""" | ||||||
|  |  | ||||||
|     name = "authentik.stages.email" |     name = "authentik.stages.email" | ||||||
|     label = "authentik_stages_email" |     label = "authentik_stages_email" | ||||||
|     verbose_name = "authentik Stages.Email" |     verbose_name = "authentik Stages.Email" | ||||||
|  |     default = True | ||||||
|  |  | ||||||
|     def ready(self): |     def reconcile_load_stages_emails_tasks(self): | ||||||
|         import_module("authentik.stages.email.tasks") |         """Load stages.emails tasks""" | ||||||
|         try: |         self.import_module("authentik.stages.email.tasks") | ||||||
|             self.validate_stage_templates() |  | ||||||
|         except ProgrammingError: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|     def validate_stage_templates(self): |     def reconcile_stage_templates_valid(self): | ||||||
|         """Ensure all stage's templates actually exist""" |         """Ensure all stage's templates actually exist""" | ||||||
|         from authentik.events.models import Event, EventAction |         from authentik.events.models import Event, EventAction | ||||||
|         from authentik.stages.email.models import EmailStage, EmailTemplates |         from authentik.stages.email.models import EmailStage, EmailTemplates | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ entries: | |||||||
|     expression: | |     expression: | | ||||||
|       # Check if we''ve not been given a username by the external IdP |       # Check if we''ve not been given a username by the external IdP | ||||||
|       # and trigger the enrollment flow |       # and trigger the enrollment flow | ||||||
|       return ''username'' not in context.get(''prompt_data'', {}) |       return 'username' not in context.get('prompt_data', {}) | ||||||
|     meta_model_name: authentik_policies_expression.expressionpolicy |     meta_model_name: authentik_policies_expression.expressionpolicy | ||||||
|   identifiers: |   identifiers: | ||||||
|     name: default-source-enrollment-if-username |     name: default-source-enrollment-if-username | ||||||
| @ -78,7 +78,7 @@ entries: | |||||||
|     order: 0 |     order: 0 | ||||||
|     stage: !KeyOf default-source-enrollment-prompt |     stage: !KeyOf default-source-enrollment-prompt | ||||||
|     target: !KeyOf flow |     target: !KeyOf flow | ||||||
|     id: prompt-binding |   id: prompt-binding | ||||||
|   model: authentik_flows.flowstagebinding |   model: authentik_flows.flowstagebinding | ||||||
| - attrs: | - attrs: | ||||||
|     evaluate_on_plan: true |     evaluate_on_plan: true | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ entries: | |||||||
|     layout: stacked |     layout: stacked | ||||||
|     name: Pre-Authentication |     name: Pre-Authentication | ||||||
|     policy_engine_mode: any |     policy_engine_mode: any | ||||||
|     title: '' |     title: Pre-authentication | ||||||
|   identifiers: |   identifiers: | ||||||
|     slug: default-source-pre-authentication |     slug: default-source-pre-authentication | ||||||
|   model: authentik_flows.flow |   model: authentik_flows.flow | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ entries: | |||||||
|     compatibility_mode: false |     compatibility_mode: false | ||||||
|     designation: stage_configuration |     designation: stage_configuration | ||||||
|     layout: stacked |     layout: stacked | ||||||
|     name: Update your info |     name: User settings | ||||||
|     policy_engine_mode: any |     policy_engine_mode: any | ||||||
|     title: '' |     title: Update your info | ||||||
|   identifiers: |   identifiers: | ||||||
|     slug: default-user-settings-flow |     slug: default-user-settings-flow | ||||||
|   model: authentik_flows.flow |   model: authentik_flows.flow | ||||||
| @ -108,9 +108,9 @@ entries: | |||||||
|  |  | ||||||
|       return True |       return True | ||||||
|     meta_model_name: authentik_policies_expression.expressionpolicy |     meta_model_name: authentik_policies_expression.expressionpolicy | ||||||
|     name: default-user-settings-authorization |  | ||||||
|   identifiers: |   identifiers: | ||||||
|     name: default-user-settings-authorization |     name: default-user-settings-authorization | ||||||
|  |   id: default-user-settings-authorization | ||||||
|   model: authentik_policies_expression.expressionpolicy |   model: authentik_policies_expression.expressionpolicy | ||||||
| - attrs: | - attrs: | ||||||
|     create_users_as_inactive: false |     create_users_as_inactive: false | ||||||
|  | |||||||
| @ -76,7 +76,6 @@ entries: | |||||||
|         - !KeyOf prompt-field-password |         - !KeyOf prompt-field-password | ||||||
|         - !KeyOf prompt-field-password-repeat |         - !KeyOf prompt-field-password-repeat | ||||||
|   - identifiers: |   - identifiers: | ||||||
|       pk: !KeyOf default-enrollment-user-login |  | ||||||
|       name: default-enrollment-user-login |       name: default-enrollment-user-login | ||||||
|     id: default-enrollment-user-login |     id: default-enrollment-user-login | ||||||
|     model: authentik_stages_user_login.userloginstage |     model: authentik_stages_user_login.userloginstage | ||||||
|  | |||||||
| @ -39,7 +39,6 @@ entries: | |||||||
|     model: authentik_stages_authenticator_validate.AuthenticatorValidateStage |     model: authentik_stages_authenticator_validate.AuthenticatorValidateStage | ||||||
|     attrs: {} |     attrs: {} | ||||||
|   - identifiers: |   - identifiers: | ||||||
|       pk: !KeyOf default-authentication-password |  | ||||||
|       name: default-authentication-password |       name: default-authentication-password | ||||||
|     id: default-authentication-password |     id: default-authentication-password | ||||||
|     model: authentik_stages_password.passwordstage |     model: authentik_stages_password.passwordstage | ||||||
|  | |||||||
| @ -93,7 +93,7 @@ entries: | |||||||
|       session_duration: seconds=0 |       session_duration: seconds=0 | ||||||
|   - identifiers: |   - identifiers: | ||||||
|       name: Change your password |       name: Change your password | ||||||
|     name: stages-prompt-password |     id: stages-prompt-password | ||||||
|     model: authentik_stages_prompt.promptstage |     model: authentik_stages_prompt.promptstage | ||||||
|     attrs: |     attrs: | ||||||
|       fields: |       fields: | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								blueprints/system/providers-oauth2.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								blueprints/system/providers-oauth2.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | version: 1 | ||||||
|  | entries: | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/oauth2/scope-openid | ||||||
|  |     model: authentik_providers_oauth2.ScopeMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OAuth Mapping: OpenID 'openid'" | ||||||
|  |       scope_name: openid | ||||||
|  |       expression: | | ||||||
|  |         # This scope is required by the OpenID-spec, and must as such exist in authentik. | ||||||
|  |         # The scope by itself does not grant any information | ||||||
|  |         return {} | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/oauth2/scope-email | ||||||
|  |     model: authentik_providers_oauth2.ScopeMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OAuth Mapping: OpenID 'email'" | ||||||
|  |       scope_name: email | ||||||
|  |       description: "Email address" | ||||||
|  |       expression: | | ||||||
|  |         return { | ||||||
|  |             "email": request.user.email, | ||||||
|  |             "email_verified": True | ||||||
|  |         } | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/oauth2/scope-profile | ||||||
|  |     model: authentik_providers_oauth2.ScopeMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OAuth Mapping: OpenID 'profile'" | ||||||
|  |       scope_name: profile | ||||||
|  |       description: "General Profile Information" | ||||||
|  |       expression: | | ||||||
|  |         return { | ||||||
|  |             # Because authentik only saves the user's full name, and has no concept of first and last names, | ||||||
|  |             # the full name is used as given name. | ||||||
|  |             # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")` | ||||||
|  |             "name": request.user.name, | ||||||
|  |             "given_name": request.user.name, | ||||||
|  |             "family_name": "", | ||||||
|  |             "preferred_username": request.user.username, | ||||||
|  |             "nickname": request.user.username, | ||||||
|  |             # groups is not part of the official userinfo schema, but is a quasi-standard | ||||||
|  |             "groups": [group.name for group in request.user.ak_groups.all()], | ||||||
|  |         } | ||||||
							
								
								
									
										17
									
								
								blueprints/system/providers-proxy.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								blueprints/system/providers-proxy.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | version: 1 | ||||||
|  | entries: | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/proxy/scope-proxy | ||||||
|  |     model: authentik_providers_oauth2.ScopeMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OAuth Mapping: Proxy outpost" | ||||||
|  |       scope_name: ak_proxy | ||||||
|  |       expression: | | ||||||
|  |         # This mapping is used by the authentik proxy. It passes extra user attributes, | ||||||
|  |         # which are used for example for the HTTP-Basic Authentication mapping. | ||||||
|  |         return { | ||||||
|  |             "ak_proxy": { | ||||||
|  |                 "user_attributes": request.user.group_attributes(request), | ||||||
|  |                 "is_superuser": request.user.is_superuser, | ||||||
|  |             } | ||||||
|  |         } | ||||||
							
								
								
									
										59
									
								
								blueprints/system/providers-saml.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								blueprints/system/providers-saml.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | version: 1 | ||||||
|  | entries: | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/upn | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: UPN" | ||||||
|  |       saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.attributes.get('upn', request.user.email) | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/name | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: Name" | ||||||
|  |       saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.name | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/email | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: Email" | ||||||
|  |       saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.email | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/username | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: Username" | ||||||
|  |       saml_name: "http://schemas.goauthentik.io/2021/02/saml/username" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.username | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/uid | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: User ID" | ||||||
|  |       saml_name: "http://schemas.goauthentik.io/2021/02/saml/uid" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.pk | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/groups | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: Groups" | ||||||
|  |       saml_name: "http://schemas.xmlsoap.org/claims/Group" | ||||||
|  |       expression: | | ||||||
|  |         for group in request.user.ak_groups.all(): | ||||||
|  |             yield group.name | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/providers/saml/ms-windowsaccountname | ||||||
|  |     model: authentik_providers_saml.SAMLPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default SAML Mapping: WindowsAccountname (Username)" | ||||||
|  |       saml_name: "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" | ||||||
|  |       expression: | | ||||||
|  |         return request.user.username | ||||||
							
								
								
									
										68
									
								
								blueprints/system/sources-ldap.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								blueprints/system/sources-ldap.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | version: 1 | ||||||
|  | entries: | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/default-name | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default LDAP Mapping: Name" | ||||||
|  |       object_field: "name" | ||||||
|  |       expression: | | ||||||
|  |         return ldap.get('name') | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/default-mail | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default LDAP Mapping: mail" | ||||||
|  |       object_field: "email" | ||||||
|  |       expression: | | ||||||
|  |         return ldap.get('mail') | ||||||
|  |   # ActiveDirectory-specific mappings | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/ms-samaccountname | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default Active Directory Mapping: sAMAccountName" | ||||||
|  |       object_field: "username" | ||||||
|  |       expression: | | ||||||
|  |         return ldap.get('sAMAccountName') | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/ms-userprincipalname | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default Active Directory Mapping: userPrincipalName" | ||||||
|  |       object_field: "attributes.upn" | ||||||
|  |       expression: | | ||||||
|  |         return list_flatten(ldap.get('userPrincipalName')) | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/ms-givenName | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default Active Directory Mapping: givenName" | ||||||
|  |       object_field: "attributes.givenName" | ||||||
|  |       expression: | | ||||||
|  |         return list_flatten(ldap.get('givenName')) | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/ms-sn | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default Active Directory Mapping: sn" | ||||||
|  |       object_field: "attributes.sn" | ||||||
|  |       expression: | | ||||||
|  |         return list_flatten(ldap.get('sn')) | ||||||
|  |   # OpenLDAP specific mappings | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/openldap-uid | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OpenLDAP Mapping: uid" | ||||||
|  |       object_field: "username" | ||||||
|  |       expression: | | ||||||
|  |         return ldap.get('uid') | ||||||
|  |   - identifiers: | ||||||
|  |       managed: goauthentik.io/sources/ldap/openldap-cn | ||||||
|  |     model: authentik_sources_ldap.LDAPPropertyMapping | ||||||
|  |     attrs: | ||||||
|  |       name: "authentik default OpenLDAP Mapping: cn" | ||||||
|  |       object_field: "name" | ||||||
|  |       expression: | | ||||||
|  |         return ldap.get('cn') | ||||||
							
								
								
									
										17
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								schema.yml
									
									
									
									
									
								
							| @ -20866,6 +20866,11 @@ components: | |||||||
|       type: object |       type: object | ||||||
|       description: Info about a single blueprint instance file |       description: Info about a single blueprint instance file | ||||||
|       properties: |       properties: | ||||||
|  |         pk: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           readOnly: true | ||||||
|  |           title: Instance uuid | ||||||
|         name: |         name: | ||||||
|           type: string |           type: string | ||||||
|         path: |         path: | ||||||
| @ -20877,15 +20882,26 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|           readOnly: true |           readOnly: true | ||||||
|  |         last_applied_hash: | ||||||
|  |           type: string | ||||||
|  |           readOnly: true | ||||||
|         status: |         status: | ||||||
|           $ref: '#/components/schemas/BlueprintInstanceStatusEnum' |           $ref: '#/components/schemas/BlueprintInstanceStatusEnum' | ||||||
|         enabled: |         enabled: | ||||||
|           type: boolean |           type: boolean | ||||||
|  |         managed_models: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             type: string | ||||||
|  |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - context |       - context | ||||||
|       - last_applied |       - last_applied | ||||||
|  |       - last_applied_hash | ||||||
|  |       - managed_models | ||||||
|       - name |       - name | ||||||
|       - path |       - path | ||||||
|  |       - pk | ||||||
|       - status |       - status | ||||||
|     BlueprintInstanceRequest: |     BlueprintInstanceRequest: | ||||||
|       type: object |       type: object | ||||||
| @ -20914,6 +20930,7 @@ components: | |||||||
|       - successful |       - successful | ||||||
|       - warning |       - warning | ||||||
|       - error |       - error | ||||||
|  |       - orphaned | ||||||
|       - unknown |       - unknown | ||||||
|       type: string |       type: string | ||||||
|     Cache: |     Cache: | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								scripts/generate_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								scripts/generate_config.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | """Generate config for development""" | ||||||
|  | from yaml import safe_dump | ||||||
|  |  | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  | with open("local.env.yml", "w") as _config: | ||||||
|  |     safe_dump( | ||||||
|  |         { | ||||||
|  |             "log_level": "debug", | ||||||
|  |             "secret_key": generate_id(), | ||||||
|  |             "postgresql": { | ||||||
|  |                 "user": "postgres", | ||||||
|  |             }, | ||||||
|  |             "outposts": { | ||||||
|  |                 "container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s", | ||||||
|  |                 "blueprint_locations": ["./blueprints"], | ||||||
|  |             }, | ||||||
|  |             "web": { | ||||||
|  |                 "outpost_port_offset": 100, | ||||||
|  |             }, | ||||||
|  |             "cert_discovery_dir": "./certs", | ||||||
|  |             "geoip": "tests/GeoLite2-City-Test.mmdb", | ||||||
|  |         }, | ||||||
|  |         _config, | ||||||
|  |         default_flow_style=False, | ||||||
|  |     ) | ||||||
| @ -13,11 +13,11 @@ from selenium.webdriver.common.keys import Keys | |||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from selenium.webdriver.support.wait import WebDriverWait | from selenium.webdriver.support.wait import WebDriverWait | ||||||
|  |  | ||||||
| from authentik.flows.models import Flow, FlowStageBinding | from authentik.blueprints import apply_blueprint | ||||||
|  | from authentik.flows.models import Flow | ||||||
| from authentik.stages.authenticator_static.models import AuthenticatorStaticStage | from authentik.stages.authenticator_static.models import AuthenticatorStaticStage | ||||||
| from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage | from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage | ||||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, retry |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -25,18 +25,16 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|     """test flow with otp stages""" |     """test flow with otp stages""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_totp_validate(self): |     def test_totp_validate(self): | ||||||
|         """test flow with otp stages""" |         """test flow with otp stages""" | ||||||
|         sleep(1) |  | ||||||
|         # Setup TOTP Device |         # Setup TOTP Device | ||||||
|         device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6) |         device = TOTPDevice.objects.create(user=self.user, confirmed=True, digits=6) | ||||||
|  |  | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|         FlowStageBinding.objects.create( |  | ||||||
|             target=flow, order=30, stage=AuthenticatorValidateStage.objects.create() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) |         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) | ||||||
|         self.login() |         self.login() | ||||||
| @ -47,16 +45,17 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|         flow_executor = self.get_shadow_root("ak-flow-executor") |         flow_executor = self.get_shadow_root("ak-flow-executor") | ||||||
|         validation_stage = self.get_shadow_root("ak-stage-authenticator-validate", flow_executor) |         validation_stage = self.get_shadow_root("ak-stage-authenticator-validate", flow_executor) | ||||||
|         code_stage = self.get_shadow_root("ak-stage-authenticator-validate-code", validation_stage) |         code_stage = self.get_shadow_root("ak-stage-authenticator-validate-code", validation_stage) | ||||||
|  |  | ||||||
|         code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(totp.token()) |         code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(totp.token()) | ||||||
|         code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER) |         code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(Keys.ENTER) | ||||||
|         self.wait_for_url(self.if_user_url("/library")) |         self.wait_for_url(self.if_user_url("/library")) | ||||||
|         self.assert_user(self.user) |         self.assert_user(self.user) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_stages_authenticator_totp", "0006_default_setup_flow") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint("blueprints/default/20-flow-default-authenticator-totp-setup.yaml") | ||||||
|     def test_totp_setup(self): |     def test_totp_setup(self): | ||||||
|         """test TOTP Setup stage""" |         """test TOTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
| @ -98,9 +97,11 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|         self.assertTrue(TOTPDevice.objects.filter(user=self.user, confirmed=True).exists()) |         self.assertTrue(TOTPDevice.objects.filter(user=self.user, confirmed=True).exists()) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_stages_authenticator_static", "0005_default_setup_flow") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint("blueprints/default/20-flow-default-authenticator-static-setup.yaml") | ||||||
|     def test_static_setup(self): |     def test_static_setup(self): | ||||||
|         """test Static OTP Setup stage""" |         """test Static OTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from selenium.webdriver.common.by import By | |||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from selenium.webdriver.support.wait import WebDriverWait | from selenium.webdriver.support.wait import WebDriverWait | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.core.tests.utils import create_test_flow | from authentik.core.tests.utils import create_test_flow | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||||
| @ -18,7 +19,7 @@ from authentik.stages.identification.models import IdentificationStage | |||||||
| from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | ||||||
| from authentik.stages.user_login.models import UserLoginStage | from authentik.stages.user_login.models import UserLoginStage | ||||||
| from authentik.stages.user_write.models import UserWriteStage | from authentik.stages.user_write.models import UserWriteStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -39,8 +40,10 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_enroll_2_step(self): |     def test_enroll_2_step(self): | ||||||
|         """Test 2-step enroll flow""" |         """Test 2-step enroll flow""" | ||||||
|         # First stage fields |         # First stage fields | ||||||
| @ -103,8 +106,10 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|         self.assertEqual(user.email, "foo@bar.baz") |         self.assertEqual(user.email, "foo@bar.baz") | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_enroll_email(self): |     def test_enroll_email(self): | ||||||
|         """Test enroll with Email verification""" |         """Test enroll with Email verification""" | ||||||
|         # First stage fields |         # First stage fields | ||||||
|  | |||||||
| @ -2,7 +2,8 @@ | |||||||
| from sys import platform | from sys import platform | ||||||
| from unittest.case import skipUnless | from unittest.case import skipUnless | ||||||
|  |  | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, retry | from authentik.blueprints import apply_blueprint | ||||||
|  | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -10,8 +11,10 @@ class TestFlowsLogin(SeleniumTestCase): | |||||||
|     """test default login flow""" |     """test default login flow""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_login(self): |     def test_login(self): | ||||||
|         """test default login flow""" |         """test default login flow""" | ||||||
|         self.driver.get( |         self.driver.get( | ||||||
|  | |||||||
| @ -5,11 +5,12 @@ from unittest.case import skipUnless | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.lib.generators import generate_key | from authentik.lib.generators import generate_key | ||||||
| from authentik.stages.password.models import PasswordStage | from authentik.stages.password.models import PasswordStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -17,9 +18,11 @@ class TestFlowsStageSetup(SeleniumTestCase): | |||||||
|     """test stage setup flows""" |     """test stage setup flows""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint("blueprints/default/0-flow-password-change.yaml") | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_stages_password", "0002_passwordstage_change_flow") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_password_change(self): |     def test_password_change(self): | ||||||
|         """test password change flow""" |         """test password change flow""" | ||||||
|         # Ensure that password stage has change_flow set |         # Ensure that password stage has change_flow set | ||||||
|  | |||||||
| @ -10,13 +10,14 @@ from guardian.shortcuts import get_anonymous_user | |||||||
| from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server | from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server | ||||||
| from ldap3.core.exceptions import LDAPInvalidCredentialsResult | from ldap3.core.exceptions import LDAPInvalidCredentialsResult | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application, User | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.outposts.managed import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | ||||||
| from authentik.providers.ldap.models import APIAccessMode, LDAPProvider | from authentik.providers.ldap.models import APIAccessMode, LDAPProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -81,8 +82,10 @@ class TestProviderLDAP(SeleniumTestCase): | |||||||
|         return outpost |         return outpost | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_ldap_bind_success(self): |     def test_ldap_bind_success(self): | ||||||
|         """Test simple bind""" |         """Test simple bind""" | ||||||
|         self._prepare() |         self._prepare() | ||||||
| @ -106,8 +109,10 @@ class TestProviderLDAP(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_ldap_bind_success_ssl(self): |     def test_ldap_bind_success_ssl(self): | ||||||
|         """Test simple bind with ssl""" |         """Test simple bind with ssl""" | ||||||
|         self._prepare() |         self._prepare() | ||||||
| @ -131,8 +136,10 @@ class TestProviderLDAP(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|     def test_ldap_bind_fail(self): |     def test_ldap_bind_fail(self): | ||||||
|         """Test simple bind (failed)""" |         """Test simple bind (failed)""" | ||||||
|         self._prepare() |         self._prepare() | ||||||
| @ -154,8 +161,11 @@ class TestProviderLDAP(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|  |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_outposts") | ||||||
|     def test_ldap_bind_search(self): |     def test_ldap_bind_search(self): | ||||||
|         """Test simple bind + search""" |         """Test simple bind + search""" | ||||||
|         outpost = self._prepare() |         outpost = self._prepare() | ||||||
|  | |||||||
| @ -8,13 +8,14 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.policies.expression.models import ExpressionPolicy | from authentik.policies.expression.models import ExpressionPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -56,10 +57,18 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OAuth Provider flow (default authorization flow with implied consent)""" |         """test OAuth Provider flow (default authorization flow with implied consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -104,10 +113,18 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OAuth Provider flow (default authorization flow with explicit consent)""" |         """test OAuth Provider flow (default authorization flow with explicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -171,10 +188,15 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_denied(self): |     def test_denied(self): | ||||||
|         """test OAuth Provider flow (default authorization flow, denied)""" |         """test OAuth Provider flow (default authorization flow, denied)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_cert | from authentik.core.tests.utils import create_test_cert | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| @ -20,7 +21,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -65,10 +66,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_redirect_uri_error(self): |     def test_redirect_uri_error(self): | ||||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" |         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -106,11 +115,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -161,11 +177,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_logout(self): |     def test_authorization_logout(self): | ||||||
|         """test OpenID Provider flow with logout""" |         """test OpenID Provider flow with logout""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -225,11 +248,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "logout").click() |         self.driver.find_element(By.ID, "logout").click() | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" |         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -298,10 +328,18 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_denied(self): |     def test_authorization_denied(self): | ||||||
|         """test OpenID Provider flow (default authorization with access deny)""" |         """test OpenID Provider flow (default authorization with access deny)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_cert | from authentik.core.tests.utils import create_test_cert | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| @ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -64,10 +65,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             sleep(1) |             sleep(1) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_redirect_uri_error(self): |     def test_redirect_uri_error(self): | ||||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" |         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -105,11 +111,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -155,11 +166,16 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         self.assertEqual(body["UserInfo"]["email"], self.user.email) |         self.assertEqual(body["UserInfo"]["email"], self.user.email) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" |         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -220,10 +236,15 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         self.assertEqual(body["UserInfo"]["email"], self.user.email) |         self.assertEqual(body["UserInfo"]["email"], self.user.email) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_denied(self): |     def test_authorization_denied(self): | ||||||
|         """test OpenID Provider flow (default authorization with access deny)""" |         """test OpenID Provider flow (default authorization with access deny)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_cert | from authentik.core.tests.utils import create_test_cert | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| @ -22,7 +23,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -64,10 +65,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|             sleep(1) |             sleep(1) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_redirect_uri_error(self): |     def test_redirect_uri_error(self): | ||||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" |         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -105,11 +111,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -150,11 +161,16 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|         self.assertEqual(body["profile"]["email"], self.user.email) |         self.assertEqual(body["profile"]["email"], self.user.email) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     @apply_blueprint("blueprints/system/providers-oauth2.yaml") | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" |         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -211,10 +227,15 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|         self.assertEqual(body["profile"]["email"], self.user.email) |         self.assertEqual(body["profile"]["email"], self.user.email) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_authorization_denied(self): |     def test_authorization_denied(self): | ||||||
|         """test OpenID Provider flow (default authorization with access deny)""" |         """test OpenID Provider flow (default authorization with access deny)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
|  | |||||||
| @ -11,12 +11,13 @@ from docker.models.containers import Container | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType | from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType | ||||||
| from authentik.outposts.tasks import outpost_local_connection | from authentik.outposts.tasks import outpost_local_connection | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -53,11 +54,19 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         return container |         return container | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-oauth2.yaml", | ||||||
|  |         "blueprints/system/providers-proxy.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_proxy_simple(self): |     def test_proxy_simple(self): | ||||||
|         """Test simple outpost setup with single provider""" |         """Test simple outpost setup with single provider""" | ||||||
|         # set additionalHeaders to test later |         # set additionalHeaders to test later | ||||||
| @ -116,11 +125,15 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase): | |||||||
|     """Test Proxy connectivity over websockets""" |     """Test Proxy connectivity over websockets""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_proxy_connectivity(self): |     def test_proxy_connectivity(self): | ||||||
|         """Test proxy connectivity over websocket""" |         """Test proxy connectivity over websocket""" | ||||||
|         outpost_local_connection() |         outpost_local_connection() | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_cert | from authentik.core.tests.utils import create_test_cert | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| @ -17,7 +18,7 @@ from authentik.policies.expression.models import ExpressionPolicy | |||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider | from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider | ||||||
| from authentik.sources.saml.processors.constants import SAML_BINDING_POST | from authentik.sources.saml.processors.constants import SAML_BINDING_POST | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -63,11 +64,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|             sleep(1) |             sleep(1) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_sp_initiated_implicit(self): |     def test_sp_initiated_implicit(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (implicit consent)""" |         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -125,11 +133,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_sp_initiated_explicit(self): |     def test_sp_initiated_explicit(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (explicit consent)""" |         """test SAML Provider flow SP-initiated flow (explicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -202,11 +217,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_sp_initiated_explicit_post(self): |     def test_sp_initiated_explicit_post(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" |         """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -279,11 +301,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_idp_initiated_implicit(self): |     def test_idp_initiated_implicit(self): | ||||||
|         """test SAML Provider flow IdP-initiated flow (implicit consent)""" |         """test SAML Provider flow IdP-initiated flow (implicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -347,11 +376,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0010_provider_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|     def test_sp_initiated_denied(self): |     def test_sp_initiated_denied(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (Policy denies access)""" |         """test SAML Provider flow SP-initiated flow (Policy denies access)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ from selenium.webdriver.support import expected_conditions as ec | |||||||
| from selenium.webdriver.support.wait import WebDriverWait | from selenium.webdriver.support.wait import WebDriverWait | ||||||
| from yaml import safe_dump | from yaml import safe_dump | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| @ -20,7 +21,7 @@ from authentik.sources.oauth.models import OAuthSource | |||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
| CONFIG_PATH = "/tmp/dex.yml"  # nosec | CONFIG_PATH = "/tmp/dex.yml"  # nosec | ||||||
|  |  | ||||||
| @ -141,11 +142,19 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|         ident_stage.save() |         ident_stage.save() | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-source-authentication.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-enrollment.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-pre-authentication.yaml", | ||||||
|  |     ) | ||||||
|     def test_oauth_enroll(self): |     def test_oauth_enroll(self): | ||||||
|         """test OAuth Source With With OIDC""" |         """test OAuth Source With With OIDC""" | ||||||
|         self.create_objects() |         self.create_objects() | ||||||
| @ -190,11 +199,14 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|         self.assert_user(User(username="foo", name="admin", email="admin@example.com")) |         self.assert_user(User(username="foo", name="admin", email="admin@example.com")) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|     def test_oauth_enroll_auth(self): |     def test_oauth_enroll_auth(self): | ||||||
|         """test OAuth Source With With OIDC (enroll and authenticate again)""" |         """test OAuth Source With With OIDC (enroll and authenticate again)""" | ||||||
|         self.test_oauth_enroll() |         self.test_oauth_enroll() | ||||||
| @ -279,11 +291,15 @@ class TestSourceOAuth1(SeleniumTestCase): | |||||||
|         ident_stage.save() |         ident_stage.save() | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @object_manager |     @apply_blueprint( | ||||||
|  |         "blueprints/default/20-flow-default-source-authentication.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-enrollment.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-pre-authentication.yaml", | ||||||
|  |     ) | ||||||
|     def test_oauth_enroll(self): |     def test_oauth_enroll(self): | ||||||
|         """test OAuth Source With With OIDC""" |         """test OAuth Source With With OIDC""" | ||||||
|         self.create_objects() |         self.create_objects() | ||||||
|  | |||||||
| @ -11,12 +11,13 @@ from selenium.webdriver.common.keys import Keys | |||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from selenium.webdriver.support.wait import WebDriverWait | from selenium.webdriver.support.wait import WebDriverWait | ||||||
|  |  | ||||||
|  | from authentik.blueprints import apply_blueprint | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, apply_migration, object_manager, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
| IDP_CERT = """-----BEGIN CERTIFICATE----- | IDP_CERT = """-----BEGIN CERTIFICATE----- | ||||||
| MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV | MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV | ||||||
| @ -94,12 +95,15 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/20-flow-default-source-authentication.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-enrollment.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-pre-authentication.yaml", | ||||||
|  |     ) | ||||||
|     def test_idp_redirect(self): |     def test_idp_redirect(self): | ||||||
|         """test SAML Source With redirect binding""" |         """test SAML Source With redirect binding""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -161,12 +165,15 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/20-flow-default-source-authentication.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-enrollment.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-pre-authentication.yaml", | ||||||
|  |     ) | ||||||
|     def test_idp_post(self): |     def test_idp_post(self): | ||||||
|         """test SAML Source With post binding""" |         """test SAML Source With post binding""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -241,12 +248,15 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_migration("authentik_flows", "0008_default_flows") |     @apply_blueprint( | ||||||
|     @apply_migration("authentik_flows", "0011_flow_title") |         "blueprints/default/10-flow-default-authentication-flow.yaml", | ||||||
|     @apply_migration("authentik_flows", "0009_source_flows") |         "blueprints/default/10-flow-default-invalidation-flow.yaml", | ||||||
|     @apply_migration("authentik_crypto", "0002_create_self_signed_kp") |     ) | ||||||
|     @apply_migration("authentik_sources_saml", "0010_samlsource_pre_authentication_flow") |     @apply_blueprint( | ||||||
|     @object_manager |         "blueprints/default/20-flow-default-source-authentication.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-enrollment.yaml", | ||||||
|  |         "blueprints/default/20-flow-default-source-pre-authentication.yaml", | ||||||
|  |     ) | ||||||
|     def test_idp_post_auto(self): |     def test_idp_post_auto(self): | ||||||
|         """test SAML Source With post binding (auto redirect)""" |         """test SAML Source With post binding (auto redirect)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|  | |||||||
| @ -10,7 +10,6 @@ from django.apps import apps | |||||||
| from django.contrib.staticfiles.testing import StaticLiveServerTestCase | from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||||||
| from django.db import connection | from django.db import connection | ||||||
| from django.db.migrations.loader import MigrationLoader | from django.db.migrations.loader import MigrationLoader | ||||||
| from django.db.migrations.operations.special import RunPython |  | ||||||
| from django.test.testcases import TransactionTestCase | from django.test.testcases import TransactionTestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from docker import DockerClient, from_env | from docker import DockerClient, from_env | ||||||
| @ -25,7 +24,7 @@ from selenium.webdriver.remote.webelement import WebElement | |||||||
| from selenium.webdriver.support.ui import WebDriverWait | from selenium.webdriver.support.ui import WebDriverWait | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.blueprints.manager import ObjectManager | from authentik.blueprints.manager import ManagedAppConfig | ||||||
| from authentik.core.api.users import UserSerializer | from authentik.core.api.users import UserSerializer | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user | ||||||
| @ -193,37 +192,22 @@ def get_loader(): | |||||||
|     return MigrationLoader(connection) |     return MigrationLoader(connection) | ||||||
|  |  | ||||||
|  |  | ||||||
| def apply_migration(app_name: str, migration_name: str): | def reconcile_app(app_name: str): | ||||||
|     """Re-apply migrations that create objects using RunPython before test cases""" |     """Re-reconcile AppConfig methods""" | ||||||
|  |  | ||||||
|     def wrapper_outter(func: Callable): |     def wrapper_outer(func: Callable): | ||||||
|         """Retry test multiple times""" |         """Re-reconcile AppConfig methods""" | ||||||
|  |  | ||||||
|         @wraps(func) |         @wraps(func) | ||||||
|         def wrapper(self: TransactionTestCase, *args, **kwargs): |         def wrapper(self: TransactionTestCase, *args, **kwargs): | ||||||
|             migration = get_loader().get_migration(app_name, migration_name) |             config = apps.get_app_config(app_name) | ||||||
|             with connection.schema_editor() as schema_editor: |             if isinstance(config, ManagedAppConfig): | ||||||
|                 for operation in migration.operations: |                 config.reconcile() | ||||||
|                     if not isinstance(operation, RunPython): |  | ||||||
|                         continue |  | ||||||
|                     operation.code(apps, schema_editor) |  | ||||||
|             return func(self, *args, **kwargs) |             return func(self, *args, **kwargs) | ||||||
|  |  | ||||||
|         return wrapper |         return wrapper | ||||||
|  |  | ||||||
|     return wrapper_outter |     return wrapper_outer | ||||||
|  |  | ||||||
|  |  | ||||||
| def object_manager(func: Callable): |  | ||||||
|     """Run objectmanager before a test function""" |  | ||||||
|  |  | ||||||
|     @wraps(func) |  | ||||||
|     def wrapper(*args, **kwargs): |  | ||||||
|         """Run objectmanager before a test function""" |  | ||||||
|         ObjectManager().run() |  | ||||||
|         return func(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     return wrapper |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def retry(max_retires=RETRIES, exceptions=None): | def retry(max_retires=RETRIES, exceptions=None): | ||||||
|  | |||||||
| @ -23,16 +23,7 @@ poetry shell # Creates a python virtualenv, and activates it in a new shell | |||||||
| poetry install # Install all required dependencies, including development dependencies | poetry install # Install all required dependencies, including development dependencies | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| To configure authentik to use the local databases, create a file in the authentik directory called `local.env.yml`, with the following contents | To configure authentik to use the local databases, we need a local config file. This file can be generated by running `make gen-dev-config`. | ||||||
|  |  | ||||||
| ```yaml |  | ||||||
| debug: true |  | ||||||
| postgresql: |  | ||||||
|     user: postgres |  | ||||||
|  |  | ||||||
| log_level: debug |  | ||||||
| secret_key: "A long key you can generate with `pwgen 40 1` for example" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream. | To apply database migrations, run `make migrate`. This is needed after the initial setup, and whenever you fetch new source from upstream. | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L