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