diff --git a/.vscode/settings.json b/.vscode/settings.json index a6c904700..25b17a816 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,9 +20,10 @@ "todo-tree.tree.showCountsInTree": true, "todo-tree.tree.showBadges": true, "python.formatting.provider": "black", - "files.associations": { - "*.akflow": "yaml" - }, + "yaml.customTags": [ + "!Find sequence", + "!KeyOf scalar" + ], "typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifierEnding": "index", "typescript.tsdk": "./web/node_modules/typescript/lib", diff --git a/Dockerfile b/Dockerfile index e433d6005..e2e5ad2d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -81,6 +81,7 @@ COPY ./pyproject.toml / COPY ./xml /xml COPY ./tests /tests COPY ./manage.py / +COPY ./blueprints/default /blueprints COPY ./lifecycle/ /lifecycle COPY --from=builder /work/authentik /authentik-proxy COPY --from=web-builder /work/web/dist/ /web/dist/ diff --git a/authentik/admin/tests/test_api.py b/authentik/admin/tests/test_api.py index 9653945b6..5ef8ff29c 100644 --- a/authentik/admin/tests/test_api.py +++ b/authentik/admin/tests/test_api.py @@ -5,10 +5,10 @@ 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 -from authentik.managed.tasks import managed_reconcile class TestAdminAPI(TestCase): diff --git a/authentik/api/v3/urls.py b/authentik/api/v3/urls.py index 75e968193..4c96cb5f6 100644 --- a/authentik/api/v3/urls.py +++ b/authentik/api/v3/urls.py @@ -12,6 +12,7 @@ from authentik.admin.api.version import VersionView from authentik.admin.api.workers import WorkerView from authentik.api.v3.config import ConfigView from authentik.api.views import APIBrowserView +from authentik.blueprints.api import BlueprintInstanceViewSet from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet @@ -131,6 +132,8 @@ router.register("events/notifications", NotificationViewSet) router.register("events/transports", NotificationTransportViewSet) router.register("events/rules", NotificationRuleViewSet) +router.register("managed/blueprints", BlueprintInstanceViewSet) + router.register("sources/all", SourceViewSet) router.register("sources/user_connections/all", UserSourceConnectionViewSet) router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet) diff --git a/authentik/flows/transfer/__init__.py b/authentik/blueprints/__init__.py similarity index 100% rename from authentik/flows/transfer/__init__.py rename to authentik/blueprints/__init__.py diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py new file mode 100644 index 000000000..3fe8e5ecc --- /dev/null +++ b/authentik/blueprints/api.py @@ -0,0 +1,56 @@ +"""Serializer mixin for managed models""" +from glob import glob + +from drf_spectacular.utils import extend_schema +from rest_framework.decorators import action +from rest_framework.fields import CharField +from rest_framework.permissions import IsAdminUser +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ListSerializer, ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from authentik.blueprints.models import BlueprintInstance +from authentik.lib.config import CONFIG + + +class ManagedSerializer: + """Managed Serializer""" + + managed = CharField(read_only=True, allow_null=True) + + +class BlueprintInstanceSerializer(ModelSerializer): + """Info about a single blueprint instance file""" + + class Meta: + + model = BlueprintInstance + fields = [ + "name", + "path", + "context", + "last_applied", + "status", + "enabled", + ] + + +class BlueprintInstanceViewSet(ModelViewSet): + """Blueprint instances""" + + permission_classes = [IsAdminUser] + serializer_class = BlueprintInstanceSerializer + queryset = BlueprintInstance.objects.all() + search_fields = ["name", "path"] + filterset_fields = ["name", "path"] + + @extend_schema(responses={200: ListSerializer(child=CharField())}) + @action(detail=False, pagination_class=None, filter_backends=[]) + def available(self, request: Request) -> Response: + """Get blueprints""" + files = [] + for folder in CONFIG.y("blueprint_locations"): + for file in glob(f"{folder}/**", recursive=True): + files.append(file) + return Response(files) diff --git a/authentik/blueprints/apps.py b/authentik/blueprints/apps.py new file mode 100644 index 000000000..e413a4eac --- /dev/null +++ b/authentik/blueprints/apps.py @@ -0,0 +1,15 @@ +"""authentik Blueprints app""" +from django.apps import AppConfig + + +class AuthentikBlueprintsConfig(AppConfig): + """authentik Blueprints app""" + + name = "authentik.blueprints" + label = "authentik_blueprints" + verbose_name = "authentik Blueprints" + + def ready(self) -> None: + from authentik.blueprints.tasks import managed_reconcile + + managed_reconcile.delay() diff --git a/authentik/managed/__init__.py b/authentik/blueprints/management/__init__.py similarity index 100% rename from authentik/managed/__init__.py rename to authentik/blueprints/management/__init__.py diff --git a/authentik/blueprints/management/commands/__init__.py b/authentik/blueprints/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/blueprints/management/commands/apply_blueprint.py b/authentik/blueprints/management/commands/apply_blueprint.py new file mode 100644 index 000000000..303ffcfcf --- /dev/null +++ b/authentik/blueprints/management/commands/apply_blueprint.py @@ -0,0 +1,22 @@ +"""Apply blueprint from commandline""" +from django.core.management.base import BaseCommand, no_translations + +from authentik.blueprints.v1.importer import Importer + + +class Command(BaseCommand): # pragma: no cover + """Apply blueprint from commandline""" + + @no_translations + def handle(self, *args, **options): + """Apply all blueprints in order, abort when one fails to import""" + 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() + if not valid: + raise ValueError("blueprint invalid") + importer.apply() + + def add_arguments(self, parser): + parser.add_argument("blueprints", nargs="+", type=str) diff --git a/authentik/managed/manager.py b/authentik/blueprints/manager.py similarity index 97% rename from authentik/managed/manager.py rename to authentik/blueprints/manager.py index 20f18fb25..e5807b515 100644 --- a/authentik/managed/manager.py +++ b/authentik/blueprints/manager.py @@ -3,7 +3,7 @@ from typing import Callable, Optional from structlog.stdlib import get_logger -from authentik.managed.models import ManagedModel +from authentik.blueprints.models import ManagedModel LOGGER = get_logger() diff --git a/authentik/blueprints/migrations/0001_initial.py b/authentik/blueprints/migrations/0001_initial.py new file mode 100644 index 000000000..5bc8ed913 --- /dev/null +++ b/authentik/blueprints/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 4.0.6 on 2022-07-30 22:45 + +import uuid + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="BlueprintInstance", + fields=[ + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "managed", + models.TextField( + default=None, + help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.", + null=True, + unique=True, + verbose_name="Managed by authentik", + ), + ), + ( + "instance_uuid", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("name", models.TextField()), + ("path", models.TextField()), + ("context", models.JSONField()), + ("last_applied", models.DateTimeField(auto_now=True)), + ( + "status", + models.TextField( + choices=[ + ("successful", "Successful"), + ("warning", "Warning"), + ("error", "Error"), + ("unknown", "Unknown"), + ] + ), + ), + ("enabled", models.BooleanField(default=True)), + ( + "managed_models", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), + ], + options={ + "verbose_name": "Blueprint Instance", + "verbose_name_plural": "Blueprint Instances", + "unique_together": {("name", "path")}, + }, + ), + ] diff --git a/authentik/blueprints/migrations/__init__.py b/authentik/blueprints/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/blueprints/models.py b/authentik/blueprints/models.py new file mode 100644 index 000000000..af20f0ee2 --- /dev/null +++ b/authentik/blueprints/models.py @@ -0,0 +1,76 @@ +"""Managed Object models""" +from uuid import uuid4 + +from django.contrib.postgres.fields import ArrayField +from django.db import models +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer + +from authentik.lib.models import CreatedUpdatedModel, SerializerModel + + +class ManagedModel(models.Model): + """Model which can be managed by authentik exclusively""" + + managed = models.TextField( + default=None, + null=True, + verbose_name=_("Managed by authentik"), + help_text=_( + ( + "Objects which are managed by authentik. These objects are created and updated " + "automatically. This is flag only indicates that an object can be overwritten by " + "migrations. You can still modify the objects via the API, but expect changes " + "to be overwritten in a later update." + ) + ), + unique=True, + ) + + class Meta: + + abstract = True + + +class BlueprintInstanceStatus(models.TextChoices): + """Instance status""" + + SUCCESSFUL = "successful" + WARNING = "warning" + ERROR = "error" + UNKNOWN = "unknown" + + +class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): + """Instance of a single blueprint. Can be parameterized via context attribute when + blueprint in `path` has inputs.""" + + instance_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + + name = models.TextField() + path = models.TextField() + context = models.JSONField() + last_applied = models.DateTimeField(auto_now=True) + status = models.TextField(choices=BlueprintInstanceStatus.choices) + enabled = models.BooleanField(default=True) + managed_models = ArrayField(models.TextField()) + + @property + def serializer(self) -> Serializer: + from authentik.blueprints.api import BlueprintInstanceSerializer + + return BlueprintInstanceSerializer + + def __str__(self) -> str: + return f"Blueprint Instance {self.name}" + + class Meta: + + verbose_name = _("Blueprint Instance") + verbose_name_plural = _("Blueprint Instances") + unique_together = ( + ( + "name", + "path", + ), + ) diff --git a/authentik/blueprints/settings.py b/authentik/blueprints/settings.py new file mode 100644 index 000000000..bd8d5842e --- /dev/null +++ b/authentik/blueprints/settings.py @@ -0,0 +1,17 @@ +"""managed 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="*"), + "options": {"queue": "authentik_scheduled"}, + }, +} diff --git a/authentik/managed/tasks.py b/authentik/blueprints/tasks.py similarity index 79% rename from authentik/managed/tasks.py rename to authentik/blueprints/tasks.py index ef7683506..c7577e3b7 100644 --- a/authentik/managed/tasks.py +++ b/authentik/blueprints/tasks.py @@ -1,6 +1,8 @@ """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, @@ -8,7 +10,6 @@ from authentik.events.monitored_tasks import ( TaskResultStatus, prefill_task, ) -from authentik.managed.manager import ObjectManager @CELERY_APP.task( @@ -24,6 +25,5 @@ def managed_reconcile(self: MonitoredTask): self.set_status( TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."]) ) - except DatabaseError as exc: # pragma: no cover + except (DatabaseError, ProgrammingError) as exc: # pragma: no cover self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)])) - self.retry() diff --git a/authentik/blueprints/tests/__init__.py b/authentik/blueprints/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/managed/tests.py b/authentik/blueprints/tests/test_managed.py similarity index 83% rename from authentik/managed/tests.py rename to authentik/blueprints/tests/test_managed.py index 85c859ffe..ccdb78949 100644 --- a/authentik/managed/tests.py +++ b/authentik/blueprints/tests/test_managed.py @@ -1,7 +1,7 @@ """managed tests""" from django.test import TestCase -from authentik.managed.tasks import managed_reconcile +from authentik.blueprints.tasks import managed_reconcile class TestManaged(TestCase): diff --git a/authentik/blueprints/tests/test_models.py b/authentik/blueprints/tests/test_models.py new file mode 100644 index 000000000..0dc9ef5a1 --- /dev/null +++ b/authentik/blueprints/tests/test_models.py @@ -0,0 +1,34 @@ +"""authentik managed models tests""" +from typing import Callable, Type + +from django.apps import apps +from django.test import TestCase + +from authentik.blueprints.v1.importer import EXCLUDED_MODELS +from authentik.lib.models import SerializerModel + + +class TestModels(TestCase): + """Test Models""" + + +def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: + """Test serializer""" + + def tester(self: TestModels): + if test_model._meta.abstract: + return + model_class = test_model() + self.assertTrue(isinstance(model_class, SerializerModel)) + self.assertIsNotNone(model_class.serializer) + + return tester + + +for app in apps.get_app_configs(): + if not app.label.startswith("authentik"): + continue + for model in app.get_models(): + if model in EXCLUDED_MODELS: + continue + setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) diff --git a/authentik/flows/tests/test_transfer.py b/authentik/blueprints/tests/test_transport.py similarity index 84% rename from authentik/flows/tests/test_transfer.py rename to authentik/blueprints/tests/test_transport.py index 5c60c478a..fded6450b 100644 --- a/authentik/flows/tests/test_transfer.py +++ b/authentik/blueprints/tests/test_transport.py @@ -1,11 +1,9 @@ -"""Test flow transfer""" +"""Test flow Transport""" from django.test import TransactionTestCase -from yaml import dump +from authentik.blueprints.v1.exporter import Exporter +from authentik.blueprints.v1.importer import Importer, transaction_rollback from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding -from authentik.flows.transfer.common import DataclassDumper -from authentik.flows.transfer.exporter import FlowExporter -from authentik.flows.transfer.importer import FlowImporter, transaction_rollback from authentik.lib.generators import generate_id from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding @@ -33,14 +31,14 @@ STATIC_PROMPT_EXPORT = """{ }""" -class TestFlowTransfer(TransactionTestCase): - """Test flow transfer""" +class TestFlowTransport(TransactionTestCase): + """Test flow Transport""" def test_bundle_invalid_format(self): """Test bundle with invalid format""" - importer = FlowImporter('{"version": 3}') + importer = Importer('{"version": 3}') self.assertFalse(importer.validate()) - importer = FlowImporter( + importer = Importer( ( '{"version": 1,"entries":[{"identifiers":{},"attrs":{},' '"model": "authentik_core.User"}]}' @@ -66,12 +64,12 @@ class TestFlowTransfer(TransactionTestCase): order=0, ) - exporter = FlowExporter(flow) + exporter = Exporter(flow) export = exporter.export() self.assertEqual(len(export.entries), 3) export_yaml = exporter.export_to_string() - importer = FlowImporter(export_yaml) + importer = Importer(export_yaml) self.assertTrue(importer.validate()) self.assertTrue(importer.apply()) @@ -81,14 +79,14 @@ class TestFlowTransfer(TransactionTestCase): """Test export and import it twice""" count_initial = Prompt.objects.filter(field_key="username").count() - importer = FlowImporter(STATIC_PROMPT_EXPORT) + importer = Importer(STATIC_PROMPT_EXPORT) self.assertTrue(importer.validate()) self.assertTrue(importer.apply()) count_before = Prompt.objects.filter(field_key="username").count() self.assertEqual(count_initial + 1, count_before) - importer = FlowImporter(STATIC_PROMPT_EXPORT) + importer = Importer(STATIC_PROMPT_EXPORT) self.assertTrue(importer.apply()) self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) @@ -114,12 +112,10 @@ class TestFlowTransfer(TransactionTestCase): fsb = FlowStageBinding.objects.create(target=flow, stage=user_login, order=0) PolicyBinding.objects.create(policy=flow_policy, target=fsb, order=0) - exporter = FlowExporter(flow) - export = exporter.export() + exporter = Exporter(flow) + export_yaml = exporter.export_to_string() - export_yaml = dump(export, Dumper=DataclassDumper) - - importer = FlowImporter(export_yaml) + importer = Importer(export_yaml) self.assertTrue(importer.validate()) self.assertTrue(importer.apply()) self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) @@ -159,11 +155,10 @@ class TestFlowTransfer(TransactionTestCase): FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0) - exporter = FlowExporter(flow) - export = exporter.export() - export_yaml = dump(export, Dumper=DataclassDumper) + exporter = Exporter(flow) + export_yaml = exporter.export_to_string() - importer = FlowImporter(export_yaml) + importer = Importer(export_yaml) self.assertTrue(importer.validate()) self.assertTrue(importer.apply()) diff --git a/authentik/flows/tests/test_transfer_docs.py b/authentik/blueprints/tests/test_transport_docs.py similarity index 63% rename from authentik/flows/tests/test_transfer_docs.py rename to authentik/blueprints/tests/test_transport_docs.py index 9148bc5e1..f40d90e59 100644 --- a/authentik/flows/tests/test_transfer_docs.py +++ b/authentik/blueprints/tests/test_transport_docs.py @@ -5,25 +5,25 @@ from typing import Callable from django.test import TransactionTestCase -from authentik.flows.transfer.importer import FlowImporter +from authentik.blueprints.v1.importer import Importer -class TestTransferDocs(TransactionTestCase): +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: TestTransferDocs): + def tester(self: TestTransportDocs): with open(file_name, "r", encoding="utf8") as flow_json: - importer = FlowImporter(flow_json.read()) + importer = Importer(flow_json.read()) self.assertTrue(importer.validate()) self.assertTrue(importer.apply()) return tester -for flow_file in glob("website/static/flows/*.akflow"): +for flow_file in glob("website/static/flows/*.yaml"): method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_") - setattr(TestTransferDocs, f"test_flow_{method_name}", pbflow_tester(flow_file)) + setattr(TestTransportDocs, f"test_flow_{method_name}", pbflow_tester(flow_file)) diff --git a/authentik/blueprints/v1/__init__.py b/authentik/blueprints/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py new file mode 100644 index 000000000..c1f29ef46 --- /dev/null +++ b/authentik/blueprints/v1/common.py @@ -0,0 +1,177 @@ +"""transfer common classes""" +from collections import OrderedDict +from dataclasses import asdict, dataclass, field, is_dataclass +from enum import Enum +from typing import Any, Optional +from uuid import UUID + +from django.apps import apps +from django.db.models import Model, Q +from rest_framework.fields import Field +from rest_framework.serializers import Serializer +from yaml import SafeDumper, SafeLoader, ScalarNode, SequenceNode + +from authentik.lib.models import SerializerModel +from authentik.lib.sentry import SentryIgnoredException + + +def get_attrs(obj: SerializerModel) -> dict[str, Any]: + """Get object's attributes via their serializer, and convert it to a normal dict""" + serializer: Serializer = obj.serializer(obj) + data = dict(serializer.data) + + for field_name, _field in serializer.fields.items(): + _field: Field + if field_name not in data: + continue + if _field.read_only: + data.pop(field_name, None) + if _field.default == data.get(field_name, None): + data.pop(field_name, None) + if field_name.endswith("_set"): + data.pop(field_name, None) + return data + + +@dataclass +class BlueprintEntry: + """Single entry of a bundle""" + + identifiers: dict[str, Any] + model: str + attrs: dict[str, Any] + + # pylint: disable=invalid-name + id: Optional[str] = None + + _instance: Optional[Model] = None + + @staticmethod + def from_model(model: SerializerModel, *extra_identifier_names: str) -> "BlueprintEntry": + """Convert a SerializerModel instance to a Bundle Entry""" + identifiers = { + "pk": model.pk, + } + all_attrs = get_attrs(model) + + for extra_identifier_name in extra_identifier_names: + identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name) + return BlueprintEntry( + identifiers=identifiers, + model=f"{model._meta.app_label}.{model._meta.model_name}", + attrs=all_attrs, + ) + + def tag_resolver(self, value: Any, blueprint: "Blueprint") -> Any: + """Check if we have any special tags that need handling""" + if isinstance(value, YAMLTag): + return value.resolve(self, blueprint) + if isinstance(value, dict): + for key, inner_value in value.items(): + value[key] = self.tag_resolver(inner_value, blueprint) + if isinstance(value, list): + for idx, inner_value in enumerate(value): + value[idx] = self.tag_resolver(inner_value, blueprint) + return value + + def get_attrs(self, blueprint: "Blueprint") -> dict[str, Any]: + """Get attributes of this entry, with all yaml tags resolved""" + return self.tag_resolver(self.attrs, blueprint) + + def get_identifiers(self, blueprint: "Blueprint") -> dict[str, Any]: + """Get attributes of this entry, with all yaml tags resolved""" + return self.tag_resolver(self.identifiers, blueprint) + + +@dataclass +class Blueprint: + """Dataclass used for a full export""" + + version: int = field(default=1) + entries: list[BlueprintEntry] = field(default_factory=list) + + +class YAMLTag: + """Base class for all YAML Tags""" + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + """Implement yaml tag logic""" + raise NotImplementedError + + +class KeyOf(YAMLTag): + """Reference another object by their ID""" + + id_from: str + + # pylint: disable=unused-argument + def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None: + super().__init__() + self.id_from = node.value + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + for _entry in blueprint.entries: + if _entry.id == self.id_from and _entry._instance: + return _entry._instance.pk + raise ValueError( + f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance" + ) + + +class Find(YAMLTag): + """Find any object""" + + model_name: str + conditions: list[list] + + model_class: type[Model] + + def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: + super().__init__() + self.model_name = node.value[0].value + self.model_class = apps.get_model(*self.model_name.split(".")) + self.conditions = [] + for raw_node in node.value[1:]: + values = [] + for node_values in raw_node.value: + values.append(loader.construct_object(node_values)) + self.conditions.append(values) + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + query = Q() + for cond in self.conditions: + query &= Q(**{cond[0]: cond[1]}) + instance = self.model_class.objects.filter(query).first() + if instance: + return instance.pk + return None + + +class BlueprintDumper(SafeDumper): + """Dump dataclasses to yaml""" + + default_flow_style = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_representer(UUID, lambda self, data: self.represent_str(str(data))) + self.add_representer(OrderedDict, lambda self, data: self.represent_dict(dict(data))) + self.add_representer(Enum, lambda self, data: self.represent_str(data.value)) + + def represent(self, data) -> None: + if is_dataclass(data): + data = asdict(data) + return super().represent(data) + + +class BlueprintLoader(SafeLoader): + """Loader for blueprints with custom tag support""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_constructor("!KeyOf", KeyOf) + self.add_constructor("!Find", Find) + + +class EntryInvalidError(SentryIgnoredException): + """Error raised when an entry is invalid""" diff --git a/authentik/flows/transfer/exporter.py b/authentik/blueprints/v1/exporter.py similarity index 69% rename from authentik/flows/transfer/exporter.py rename to authentik/blueprints/v1/exporter.py index 3d1a8c56f..1eed6b2e3 100644 --- a/authentik/flows/transfer/exporter.py +++ b/authentik/blueprints/v1/exporter.py @@ -5,14 +5,14 @@ from uuid import UUID from django.db.models import Q from yaml import dump +from authentik.blueprints.v1.common import Blueprint, BlueprintDumper, BlueprintEntry from authentik.flows.models import Flow, FlowStageBinding, Stage -from authentik.flows.transfer.common import DataclassDumper, FlowBundle, FlowBundleEntry from authentik.policies.models import Policy, PolicyBinding from authentik.stages.prompt.models import PromptStage -class FlowExporter: - """Export flow with attached stages into json""" +class Exporter: + """Export flow with attached stages into yaml""" flow: Flow with_policies: bool @@ -31,21 +31,21 @@ class FlowExporter: "pbm_uuid", flat=True ) - def walk_stages(self) -> Iterator[FlowBundleEntry]: - """Convert all stages attached to self.flow into FlowBundleEntry objects""" + def walk_stages(self) -> Iterator[BlueprintEntry]: + """Convert all stages attached to self.flow into BlueprintEntry objects""" stages = Stage.objects.filter(flow=self.flow).select_related().select_subclasses() for stage in stages: if isinstance(stage, PromptStage): pass - yield FlowBundleEntry.from_model(stage, "name") + yield BlueprintEntry.from_model(stage, "name") - def walk_stage_bindings(self) -> Iterator[FlowBundleEntry]: - """Convert all bindings attached to self.flow into FlowBundleEntry objects""" + def walk_stage_bindings(self) -> Iterator[BlueprintEntry]: + """Convert all bindings attached to self.flow into BlueprintEntry objects""" bindings = FlowStageBinding.objects.filter(target=self.flow).select_related() for binding in bindings: - yield FlowBundleEntry.from_model(binding, "target", "stage", "order") + yield BlueprintEntry.from_model(binding, "target", "stage", "order") - def walk_policies(self) -> Iterator[FlowBundleEntry]: + def walk_policies(self) -> Iterator[BlueprintEntry]: """Walk over all policies. This is done at the beginning of the export for stages that have a direct foreign key to a policy.""" # Special case for PromptStage as that has a direct M2M to policy, we have to ensure @@ -54,28 +54,28 @@ class FlowExporter: query = Q(bindings__in=self.pbm_uuids) | Q(promptstage__in=prompt_stages) policies = Policy.objects.filter(query).select_related() for policy in policies: - yield FlowBundleEntry.from_model(policy) + yield BlueprintEntry.from_model(policy) - def walk_policy_bindings(self) -> Iterator[FlowBundleEntry]: + def walk_policy_bindings(self) -> Iterator[BlueprintEntry]: """Walk over all policybindings relative to us. This is run at the end of the export, as we are sure all objects exist now.""" bindings = PolicyBinding.objects.filter(target__in=self.pbm_uuids).select_related() for binding in bindings: - yield FlowBundleEntry.from_model(binding, "policy", "target", "order") + yield BlueprintEntry.from_model(binding, "policy", "target", "order") - def walk_stage_prompts(self) -> Iterator[FlowBundleEntry]: + def walk_stage_prompts(self) -> Iterator[BlueprintEntry]: """Walk over all prompts associated with any PromptStages""" prompt_stages = PromptStage.objects.filter(flow=self.flow) for stage in prompt_stages: for prompt in stage.fields.all(): - yield FlowBundleEntry.from_model(prompt) + yield BlueprintEntry.from_model(prompt) - def export(self) -> FlowBundle: + def export(self) -> Blueprint: """Create a list of all objects including the flow""" if self.with_policies: self._prepare_pbm() - bundle = FlowBundle() - bundle.entries.append(FlowBundleEntry.from_model(self.flow, "slug")) + bundle = Blueprint() + bundle.entries.append(BlueprintEntry.from_model(self.flow, "slug")) if self.with_stage_prompts: bundle.entries.extend(self.walk_stage_prompts()) if self.with_policies: @@ -87,6 +87,6 @@ class FlowExporter: return bundle def export_to_string(self) -> str: - """Call export and convert it to json""" + """Call export and convert it to yaml""" bundle = self.export() - return dump(bundle, Dumper=DataclassDumper) + return dump(bundle, Dumper=BlueprintDumper) diff --git a/authentik/flows/transfer/importer.py b/authentik/blueprints/v1/importer.py similarity index 79% rename from authentik/flows/transfer/importer.py rename to authentik/blueprints/v1/importer.py index b9b024d9a..bbbb70fae 100644 --- a/authentik/flows/transfer/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -1,4 +1,4 @@ -"""Flow importer""" +"""Blueprint importer""" from contextlib import contextmanager from copy import deepcopy from typing import Any @@ -13,15 +13,39 @@ 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 yaml import safe_load +from yaml import load -from authentik.flows.models import Flow, FlowStageBinding, Stage -from authentik.flows.transfer.common import EntryInvalidError, FlowBundle, FlowBundleEntry +from authentik.blueprints.v1.common import ( + Blueprint, + BlueprintEntry, + BlueprintLoader, + EntryInvalidError, +) +from authentik.core.models import ( + AuthenticatedSession, + PropertyMapping, + Provider, + Source, + UserSourceConnection, +) +from authentik.flows.models import Stage from authentik.lib.models import SerializerModel -from authentik.policies.models import Policy, PolicyBinding -from authentik.stages.prompt.models import Prompt +from authentik.outposts.models import OutpostServiceConnection +from authentik.policies.models import Policy, PolicyBindingModel -ALLOWED_MODELS = (Flow, FlowStageBinding, Stage, Policy, PolicyBinding, Prompt) +EXCLUDED_MODELS = ( + # Base classes + Provider, + Source, + PropertyMapping, + UserSourceConnection, + Stage, + OutpostServiceConnection, + Policy, + PolicyBindingModel, + # Classes that have other dependencies + AuthenticatedSession, +) @contextmanager @@ -34,17 +58,17 @@ def transaction_rollback(): atomic.__exit__(IntegrityError, None, None) -class FlowImporter: - """Import Flow from json""" +class Importer: + """Import Blueprint from YAML""" logger: BoundLogger def __init__(self, yaml_input: str): self.__pk_map: dict[Any, Model] = {} self.logger = get_logger() - import_dict = safe_load(yaml_input) + import_dict = load(yaml_input, BlueprintLoader) try: - self.__import = from_dict(FlowBundle, import_dict) + self.__import = from_dict(Blueprint, import_dict) except DaciteError as exc: raise EntryInvalidError from exc @@ -75,7 +99,9 @@ class FlowImporter: """Generate an or'd query from all identifiers in an entry""" # Since identifiers can also be pk-references to other objects (see FlowStageBinding) # we have to ensure those references are also replaced - main_query = Q(pk=attrs["pk"]) + main_query = Q() + if "pk" in attrs: + main_query = Q(pk=attrs["pk"]) sub_query = Q() for identifier, value in attrs.items(): if isinstance(value, dict): @@ -85,11 +111,12 @@ class FlowImporter: sub_query &= Q(**{identifier: value}) return main_query | sub_query - def _validate_single(self, entry: FlowBundleEntry) -> BaseSerializer: + def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer: """Validate a single entry""" model_app_label, model_name = entry.model.split(".") model: type[SerializerModel] = apps.get_model(model_app_label, model_name) - if not isinstance(model(), ALLOWED_MODELS): + # Don't use isinstance since we don't want to check for inheritance + if model in EXCLUDED_MODELS: raise EntryInvalidError(f"Model {model} not allowed") # If we try to validate without referencing a possible instance @@ -97,7 +124,7 @@ class FlowImporter: # the full serializer for later usage # Because a model might have multiple unique columns, we chain all identifiers together # to create an OR query. - updated_identifiers = self.__update_pks_for_attrs(entry.identifiers) + updated_identifiers = self.__update_pks_for_attrs(entry.get_identifiers(self.__import)) for key, value in list(updated_identifiers.items()): if isinstance(value, dict) and "pk" in value: del updated_identifiers[key] @@ -121,7 +148,7 @@ class FlowImporter: if "pk" in updated_identifiers: model_instance.pk = updated_identifiers["pk"] serializer_kwargs["instance"] = model_instance - full_data = self.__update_pks_for_attrs(entry.attrs) + full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import)) full_data.update(updated_identifiers) serializer_kwargs["data"] = full_data @@ -133,7 +160,7 @@ class FlowImporter: return serializer def apply(self) -> bool: - """Apply (create/update) flow json, in database transaction""" + """Apply (create/update) models yaml, in database transaction""" try: with transaction.atomic(): if not self._apply_models(): @@ -146,10 +173,9 @@ class FlowImporter: return True def _apply_models(self) -> bool: - """Apply (create/update) flow json""" + """Apply (create/update) models yaml""" self.__pk_map = {} - entries = deepcopy(self.__import.entries) - for entry in entries: + for entry in self.__import.entries: model_app_label, model_name = entry.model.split(".") try: model: SerializerModel = apps.get_model(model_app_label, model_name) @@ -166,7 +192,9 @@ class FlowImporter: return False model = serializer.save() - self.__pk_map[entry.identifiers["pk"]] = model.pk + if "pk" in entry.identifiers: + self.__pk_map[entry.identifiers["pk"]] = model.pk + entry._instance = model self.logger.debug("updated model", model=model, pk=model.pk) return True @@ -174,6 +202,7 @@ class FlowImporter: """Validate loaded flow export, ensure all models are allowed and serializers have no errors""" self.logger.debug("Starting flow import validation") + orig_import = deepcopy(self.__import) if self.__import.version != 1: self.logger.warning("Invalid bundle version") return False @@ -181,4 +210,5 @@ class FlowImporter: successful = self._apply_models() if not successful: self.logger.debug("Flow validation failed") + self.__import = orig_import return successful diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index 54e73cb2f..0a5cace89 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -14,12 +14,12 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.viewsets import GenericViewSet from authentik.api.decorators import permission_required +from authentik.blueprints.api import ManagedSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer from authentik.core.expression import PropertyMappingEvaluator from authentik.core.models import PropertyMapping from authentik.lib.utils.reflection import all_subclasses -from authentik.managed.api import ManagedSerializer from authentik.policies.api.exec import PolicyTestSerializer diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index c923e97f2..0bec16868 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -15,13 +15,13 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.decorators import permission_required +from authentik.blueprints.api import ManagedSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.users import UserSerializer from authentik.core.api.utils import PassiveSerializer from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents from authentik.events.models import Event, EventAction from authentik.events.utils import model_to_dict -from authentik.managed.api import ManagedSerializer class TokenSerializer(ManagedSerializer, ModelSerializer): diff --git a/authentik/core/managed.py b/authentik/core/managed.py index e377c296f..627a9940d 100644 --- a/authentik/core/managed.py +++ b/authentik/core/managed.py @@ -1,6 +1,6 @@ """Core managed objects""" +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.core.models import Source -from authentik.managed.manager import EnsureExists, ObjectManager class CoreManager(ObjectManager): diff --git a/authentik/core/models.py b/authentik/core/models.py index c6e6cc7c3..ed745caf8 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -20,9 +20,10 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from guardian.mixins import GuardianUserMixin from model_utils.managers import InheritanceManager -from rest_framework.serializers import Serializer +from rest_framework.serializers import BaseSerializer, Serializer from structlog.stdlib import get_logger +from authentik.blueprints.models import ManagedModel from authentik.core.exceptions import PropertyMappingExpressionException from authentik.core.signals import password_changed from authentik.core.types import UILoginButton, UserSettingSerializer @@ -30,7 +31,6 @@ from authentik.lib.config import CONFIG, get_path_from_dict from authentik.lib.generators import generate_id from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel from authentik.lib.utils.http import get_client_ip -from authentik.managed.models import ManagedModel from authentik.policies.models import PolicyBindingModel LOGGER = get_logger() @@ -68,7 +68,7 @@ def default_token_key(): return generate_id(int(CONFIG.y("default_token_length"))) -class Group(models.Model): +class Group(SerializerModel): """Custom Group model which supports a basic hierarchy""" group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -87,6 +87,12 @@ class Group(models.Model): ) attributes = models.JSONField(default=dict, blank=True) + @property + def serializer(self) -> Serializer: + from authentik.core.api.groups import GroupSerializer + + return GroupSerializer + @property def num_pk(self) -> int: """Get a numerical, int32 ID for the group""" @@ -139,7 +145,7 @@ class UserManager(DjangoUserManager): return self._create_user(username, email, password, **extra_fields) -class User(GuardianUserMixin, AbstractUser): +class User(SerializerModel, GuardianUserMixin, AbstractUser): """Custom User model to allow easier adding of user-based settings""" uuid = models.UUIDField(default=uuid4, editable=False) @@ -170,6 +176,12 @@ class User(GuardianUserMixin, AbstractUser): always_merger.merge(final_attributes, self.attributes) return final_attributes + @property + def serializer(self) -> Serializer: + from authentik.core.api.users import UserSerializer + + return UserSerializer + @cached_property def is_superuser(self) -> bool: """Get supseruser status based on membership in a group with superuser status""" @@ -276,7 +288,7 @@ class Provider(SerializerModel): return self.name -class Application(PolicyBindingModel): +class Application(SerializerModel, PolicyBindingModel): """Every Application which uses authentik for authentication/identification/authorization needs an Application record. Other authentication types can subclass this Model to add custom fields and other properties""" @@ -307,6 +319,12 @@ class Application(PolicyBindingModel): meta_description = models.TextField(default="", blank=True) meta_publisher = models.TextField(default="", blank=True) + @property + def serializer(self) -> Serializer: + from authentik.core.api.applications import ApplicationSerializer + + return ApplicationSerializer + @property def get_meta_icon(self) -> Optional[str]: """Get the URL to the App Icon image. If the name is /static or starts with http @@ -454,7 +472,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): return self.name -class UserSourceConnection(CreatedUpdatedModel): +class UserSourceConnection(SerializerModel, CreatedUpdatedModel): """Connection between User and Source.""" user = models.ForeignKey(User, on_delete=models.CASCADE) @@ -462,6 +480,11 @@ class UserSourceConnection(CreatedUpdatedModel): objects = InheritanceManager() + @property + def serializer(self) -> BaseSerializer: + """Get serializer for this model""" + raise NotImplementedError + class Meta: unique_together = (("user", "source"),) @@ -516,7 +539,7 @@ class TokenIntents(models.TextChoices): INTENT_APP_PASSWORD = "app_password" # nosec -class Token(ManagedModel, ExpiringModel): +class Token(SerializerModel, ManagedModel, ExpiringModel): """Token used to authenticate the User for API Access or confirm another Stage like Email.""" token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -528,6 +551,12 @@ class Token(ManagedModel, ExpiringModel): user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+") description = models.TextField(default="", blank=True) + @property + def serializer(self) -> Serializer: + from authentik.core.api.tokens import TokenSerializer + + return TokenSerializer + def expire_action(self, *args, **kwargs): """Handler which is called when this object is expired.""" from authentik.events.models import Event, EventAction diff --git a/authentik/crypto/managed.py b/authentik/crypto/managed.py index ef4f96040..3eb2198b6 100644 --- a/authentik/crypto/managed.py +++ b/authentik/crypto/managed.py @@ -2,9 +2,9 @@ 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 -from authentik.managed.manager import ObjectManager MANAGED_KEY = "goauthentik.io/crypto/jwt-managed" diff --git a/authentik/crypto/models.py b/authentik/crypto/models.py index 2f881a2f4..5badd247e 100644 --- a/authentik/crypto/models.py +++ b/authentik/crypto/models.py @@ -16,15 +16,16 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.x509 import Certificate, load_pem_x509_certificate from django.db import models from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer from structlog.stdlib import get_logger -from authentik.lib.models import CreatedUpdatedModel -from authentik.managed.models import ManagedModel +from authentik.blueprints.models import ManagedModel +from authentik.lib.models import CreatedUpdatedModel, SerializerModel LOGGER = get_logger() -class CertificateKeyPair(ManagedModel, CreatedUpdatedModel): +class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel): """CertificateKeyPair that can be used for signing or encrypting if `key_data` is set, otherwise it can be used to verify remote data.""" @@ -44,6 +45,12 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel): _private_key: Optional[RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey] = None _public_key: Optional[RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey] = None + @property + def serializer(self) -> Serializer: + from authentik.crypto.api import CertificateKeyPairSerializer + + return CertificateKeyPairSerializer + @property def certificate(self) -> Certificate: """Get python cryptography Certificate instance""" diff --git a/authentik/events/models.py b/authentik/events/models.py index d1bdc178f..f9b06d71a 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -30,7 +30,7 @@ from authentik.core.middleware import ( from authentik.core.models import ExpiringModel, Group, PropertyMapping, User from authentik.events.geo import GEOIP_READER from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict -from authentik.lib.models import DomainlessURLValidator +from authentik.lib.models import DomainlessURLValidator, SerializerModel from authentik.lib.sentry import SentryIgnoredException from authentik.lib.utils.http import get_client_ip, get_http_session from authentik.lib.utils.time import timedelta_from_string @@ -168,7 +168,7 @@ class EventManager(Manager): return self.get_queryset().get_events_per_day() -class Event(ExpiringModel): +class Event(SerializerModel, ExpiringModel): """An individual Audit/Metrics/Notification/Error Event""" event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -273,6 +273,12 @@ class Event(ExpiringModel): ) super().save(*args, **kwargs) + @property + def serializer(self) -> "Serializer": + from authentik.events.api.events import EventSerializer + + return EventSerializer + @property def summary(self) -> str: """Return a summary of this event.""" @@ -298,7 +304,7 @@ class TransportMode(models.TextChoices): EMAIL = "email", _("Email") -class NotificationTransport(models.Model): +class NotificationTransport(SerializerModel): """Action which is executed when a Rule matches""" uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -448,6 +454,12 @@ class NotificationTransport(models.Model): except (SMTPException, ConnectionError, OSError) as exc: raise NotificationTransportError from exc + @property + def serializer(self) -> "Serializer": + from authentik.events.api.notification_transports import NotificationTransportSerializer + + return NotificationTransportSerializer + def __str__(self) -> str: return f"Notification Transport {self.name}" @@ -465,7 +477,7 @@ class NotificationSeverity(models.TextChoices): ALERT = "alert", _("Alert") -class Notification(models.Model): +class Notification(SerializerModel): """Event Notification""" uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -476,6 +488,12 @@ class Notification(models.Model): seen = models.BooleanField(default=False) user = models.ForeignKey(User, on_delete=models.CASCADE) + @property + def serializer(self) -> "Serializer": + from authentik.events.api.notifications import NotificationSerializer + + return NotificationSerializer + def __str__(self) -> str: body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body return f"Notification for user {self.user}: {body_trunc}" @@ -486,7 +504,7 @@ class Notification(models.Model): verbose_name_plural = _("Notifications") -class NotificationRule(PolicyBindingModel): +class NotificationRule(SerializerModel, PolicyBindingModel): """Decide when to create a Notification based on policies attached to this object.""" name = models.TextField(unique=True) @@ -518,6 +536,12 @@ class NotificationRule(PolicyBindingModel): on_delete=models.SET_NULL, ) + @property + def serializer(self) -> "Serializer": + from authentik.events.api.notification_rules import NotificationRuleSerializer + + return NotificationRuleSerializer + def __str__(self) -> str: return f"Notification Rule {self.name}" diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index 9015a94c4..c7877cb6c 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -20,6 +20,8 @@ from rest_framework.viewsets import ModelViewSet from structlog.stdlib import get_logger from authentik.api.decorators import permission_required +from authentik.blueprints.v1.exporter import Exporter +from authentik.blueprints.v1.importer import Importer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import ( CacheSerializer, @@ -30,8 +32,6 @@ from authentik.core.api.utils import ( from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.models import Flow from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key -from authentik.flows.transfer.exporter import FlowExporter -from authentik.flows.transfer.importer import FlowImporter from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN from authentik.lib.views import bad_request_message @@ -163,11 +163,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet): ) @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,)) def import_flow(self, request: Request) -> Response: - """Import flow from .akflow file""" + """Import flow from .yaml file""" file = request.FILES.get("file", None) if not file: return HttpResponseBadRequest() - importer = FlowImporter(file.read().decode()) + importer = Importer(file.read().decode()) valid = importer.validate() if not valid: return HttpResponseBadRequest() @@ -195,11 +195,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet): @action(detail=True, pagination_class=None, filter_backends=[]) # pylint: disable=unused-argument def export(self, request: Request, slug: str) -> Response: - """Export flow to .akflow file""" + """Export flow to .yaml file""" flow = self.get_object() - exporter = FlowExporter(flow) + exporter = Exporter(flow) response = HttpResponse(content=exporter.export_to_string()) - response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"' + response["Content-Disposition"] = f'attachment; filename="{flow.slug}.yaml"' return response @extend_schema(responses={200: FlowDiagramSerializer()}) diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index 2933f0284..225da0a39 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -1,14 +1,16 @@ """Challenge helpers""" +from dataclasses import asdict, is_dataclass from enum import Enum from typing import TYPE_CHECKING, Optional, TypedDict +from uuid import UUID +from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.http import JsonResponse from rest_framework.fields import ChoiceField, DictField from rest_framework.serializers import CharField from authentik.core.api.utils import PassiveSerializer -from authentik.flows.transfer.common import DataclassEncoder if TYPE_CHECKING: from authentik.flows.stage import StageView @@ -135,6 +137,19 @@ class AutoSubmitChallengeResponse(ChallengeResponse): component = CharField(default="ak-stage-autosubmit") +class DataclassEncoder(DjangoJSONEncoder): + """Convert any dataclass to json""" + + def default(self, o): + if is_dataclass(o): + return asdict(o) + if isinstance(o, UUID): + return str(o) + if isinstance(o, Enum): + return o.value + return super().default(o) # pragma: no cover + + class HttpChallengeResponse(JsonResponse): """Subclass of JsonResponse that uses the `DataclassEncoder`""" diff --git a/authentik/flows/management/commands/apply_flow.py b/authentik/flows/management/commands/apply_flow.py deleted file mode 100644 index 0081df0c8..000000000 --- a/authentik/flows/management/commands/apply_flow.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Apply flow from commandline""" -from django.core.management.base import BaseCommand, no_translations - -from authentik.flows.transfer.importer import FlowImporter - - -class Command(BaseCommand): # pragma: no cover - """Apply flow from commandline""" - - @no_translations - def handle(self, *args, **options): - """Apply all flows in order, abort when one fails to import""" - for flow_path in options.get("flows", []): - with open(flow_path, "r", encoding="utf8") as flow_file: - importer = FlowImporter(flow_file.read()) - valid = importer.validate() - if not valid: - raise ValueError("Flow invalid") - importer.apply() - - def add_arguments(self, parser): - parser.add_argument("flows", nargs="+", type=str) diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py deleted file mode 100644 index 193754e9d..000000000 --- a/authentik/flows/transfer/common.py +++ /dev/null @@ -1,105 +0,0 @@ -"""transfer common classes""" -from dataclasses import asdict, dataclass, field, is_dataclass -from enum import Enum -from typing import Any -from uuid import UUID - -from django.core.serializers.json import DjangoJSONEncoder -from yaml import SafeDumper - -from authentik.lib.models import SerializerModel -from authentik.lib.sentry import SentryIgnoredException - - -def get_attrs(obj: SerializerModel) -> dict[str, Any]: - """Get object's attributes via their serializer, and convert it to a normal dict""" - data = dict(obj.serializer(obj).data) - to_remove = ( - "policies", - "stages", - "pk", - "background", - "group", - "user", - "verbose_name", - "verbose_name_plural", - "component", - "flow_set", - "promptstage_set", - "policybindingmodel_ptr_id", - "export_url", - "meta_model_name", - ) - for to_remove_name in to_remove: - if to_remove_name in data: - data.pop(to_remove_name) - for key in list(data.keys()): - if key.endswith("_obj"): - data.pop(key) - return data - - -@dataclass -class FlowBundleEntry: - """Single entry of a bundle""" - - identifiers: dict[str, Any] - model: str - attrs: dict[str, Any] - - @staticmethod - def from_model(model: SerializerModel, *extra_identifier_names: str) -> "FlowBundleEntry": - """Convert a SerializerModel instance to a Bundle Entry""" - identifiers = { - "pk": model.pk, - } - all_attrs = get_attrs(model) - - for extra_identifier_name in extra_identifier_names: - identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name) - return FlowBundleEntry( - identifiers=identifiers, - model=f"{model._meta.app_label}.{model._meta.model_name}", - attrs=all_attrs, - ) - - -@dataclass -class FlowBundle: - """Dataclass used for a full export""" - - version: int = field(default=1) - entries: list[FlowBundleEntry] = field(default_factory=list) - - -class DataclassEncoder(DjangoJSONEncoder): - """Convert FlowBundleEntry to json""" - - def default(self, o): - if is_dataclass(o): - return asdict(o) - if isinstance(o, UUID): - return str(o) - if isinstance(o, Enum): - return o.value - return super().default(o) # pragma: no cover - - -class DataclassDumper(SafeDumper): - """Dump dataclasses to yaml""" - - default_flow_style = False - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.add_representer(UUID, lambda self, data: self.represent_str(str(data))) - self.add_representer(Enum, lambda self, data: self.represent_str(data.value)) - - def represent(self, data) -> None: - if is_dataclass(data): - data = asdict(data) - return super().represent(data) - - -class EntryInvalidError(SentryIgnoredException): - """Error raised when an entry is invalid""" diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 948600922..75e1f8bb7 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -62,6 +62,7 @@ ldap: tls: ciphers: null +config_file_dir: "/config" cookie_domain: null disable_update_check: false disable_startup_analytics: false diff --git a/authentik/lib/utils/template.py b/authentik/lib/utils/template.py deleted file mode 100644 index a5486164a..000000000 --- a/authentik/lib/utils/template.py +++ /dev/null @@ -1,8 +0,0 @@ -"""authentik lib template utilities""" -from django.template import Context, loader - - -def render_to_string(template_path: str, ctx: Context) -> str: - """Render a template to string""" - template = loader.get_template(template_path) - return template.render(ctx) diff --git a/authentik/managed/api.py b/authentik/managed/api.py deleted file mode 100644 index a3e8196c5..000000000 --- a/authentik/managed/api.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Serializer mixin for managed models""" -from rest_framework.fields import CharField - - -class ManagedSerializer: - """Managed Serializer""" - - managed = CharField(read_only=True, allow_null=True) diff --git a/authentik/managed/apps.py b/authentik/managed/apps.py deleted file mode 100644 index b465ae797..000000000 --- a/authentik/managed/apps.py +++ /dev/null @@ -1,15 +0,0 @@ -"""authentik Managed app""" -from django.apps import AppConfig - - -class AuthentikManagedConfig(AppConfig): - """authentik Managed app""" - - name = "authentik.managed" - label = "authentik_managed" - verbose_name = "authentik Managed" - - def ready(self) -> None: - from authentik.managed.tasks import managed_reconcile - - managed_reconcile.delay() diff --git a/authentik/managed/models.py b/authentik/managed/models.py deleted file mode 100644 index 508cc8102..000000000 --- a/authentik/managed/models.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Managed Object models""" -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class ManagedModel(models.Model): - """Model which can be managed by authentik exclusively""" - - managed = models.TextField( - default=None, - null=True, - verbose_name=_("Managed by authentik"), - help_text=_( - ( - "Objects which are managed by authentik. These objects are created and updated " - "automatically. This is flag only indicates that an object can be overwritten by " - "migrations. You can still modify the objects via the API, but expect changes " - "to be overwritten in a later update." - ) - ), - unique=True, - ) - - class Meta: - - abstract = True diff --git a/authentik/managed/settings.py b/authentik/managed/settings.py deleted file mode 100644 index ebb2edb95..000000000 --- a/authentik/managed/settings.py +++ /dev/null @@ -1,12 +0,0 @@ -"""managed Settings""" -from celery.schedules import crontab - -from authentik.lib.utils.time import fqdn_rand - -CELERY_BEAT_SCHEDULE = { - "managed_reconcile": { - "task": "authentik.managed.tasks.managed_reconcile", - "schedule": crontab(minute=fqdn_rand("managed_reconcile"), hour="*/4"), - "options": {"queue": "authentik_scheduled"}, - }, -} diff --git a/authentik/outposts/managed.py b/authentik/outposts/managed.py index 41ce76f4a..d8bfd5964 100644 --- a/authentik/outposts/managed.py +++ b/authentik/outposts/managed.py @@ -1,5 +1,5 @@ """Outpost managed objects""" -from authentik.managed.manager import EnsureExists, ObjectManager +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.outposts.models import ( DockerServiceConnection, KubernetesServiceConnection, diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 7eeb5e20d..d81aa0e11 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -14,9 +14,11 @@ from guardian.models import UserObjectPermission from guardian.shortcuts import assign_perm from model_utils.managers import InheritanceManager from packaging.version import LegacyVersion, Version, parse +from rest_framework.serializers import Serializer from structlog.stdlib import get_logger from authentik import __version__, get_build_hash +from authentik.blueprints.models import ManagedModel from authentik.core.models import ( USER_ATTRIBUTE_CAN_OVERRIDE_IP, USER_ATTRIBUTE_SA, @@ -29,10 +31,9 @@ from authentik.core.models import ( from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction from authentik.lib.config import CONFIG -from authentik.lib.models import InheritanceForeignKey +from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.lib.sentry import SentryIgnoredException from authentik.lib.utils.errors import exception_to_string -from authentik.managed.models import ManagedModel from authentik.outposts.controllers.k8s.utils import get_namespace from authentik.tenants.models import Tenant @@ -155,7 +156,7 @@ class OutpostServiceConnection(models.Model): verbose_name_plural = _("Outpost Service-Connections") -class DockerServiceConnection(OutpostServiceConnection): +class DockerServiceConnection(SerializerModel, OutpostServiceConnection): """Service Connection to a Docker endpoint""" url = models.TextField( @@ -192,6 +193,12 @@ class DockerServiceConnection(OutpostServiceConnection): ), ) + @property + def serializer(self) -> Serializer: + from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer + + return DockerServiceConnectionSerializer + @property def component(self) -> str: return "ak-service-connection-docker-form" @@ -205,7 +212,7 @@ class DockerServiceConnection(OutpostServiceConnection): verbose_name_plural = _("Docker Service-Connections") -class KubernetesServiceConnection(OutpostServiceConnection): +class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection): """Service Connection to a Kubernetes cluster""" kubeconfig = models.JSONField( @@ -218,6 +225,12 @@ class KubernetesServiceConnection(OutpostServiceConnection): blank=True, ) + @property + def serializer(self) -> Serializer: + from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer + + return KubernetesServiceConnectionSerializer + @property def component(self) -> str: return "ak-service-connection-kubernetes-form" @@ -231,7 +244,7 @@ class KubernetesServiceConnection(OutpostServiceConnection): verbose_name_plural = _("Kubernetes Service-Connections") -class Outpost(ManagedModel): +class Outpost(SerializerModel, ManagedModel): """Outpost instance which manages a service user and token""" uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) @@ -256,6 +269,12 @@ class Outpost(ManagedModel): providers = models.ManyToManyField(Provider) + @property + def serializer(self) -> Serializer: + from authentik.outposts.api.outposts import OutpostSerializer + + return OutpostSerializer + @property def config(self) -> OutpostConfig: """Load config as OutpostConfig object""" diff --git a/authentik/outposts/tests/test_controller_docker.py b/authentik/outposts/tests/test_controller_docker.py index 7d47d1ac4..6757a34b7 100644 --- a/authentik/outposts/tests/test_controller_docker.py +++ b/authentik/outposts/tests/test_controller_docker.py @@ -2,7 +2,7 @@ from django.test import TestCase from docker.models.containers import Container -from authentik.managed.manager import ObjectManager +from authentik.blueprints.manager import ObjectManager from authentik.outposts.controllers.base import ControllerException from authentik.outposts.controllers.docker import DockerController from authentik.outposts.managed import MANAGED_OUTPOST diff --git a/authentik/policies/event_matcher/migrations/0001_squashed_0018_alter_eventmatcherpolicy_action.py b/authentik/policies/event_matcher/migrations/0001_squashed_0018_alter_eventmatcherpolicy_action.py index af6024573..dca5a3d91 100644 --- a/authentik/policies/event_matcher/migrations/0001_squashed_0018_alter_eventmatcherpolicy_action.py +++ b/authentik/policies/event_matcher/migrations/0001_squashed_0018_alter_eventmatcherpolicy_action.py @@ -158,7 +158,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.tenants", "authentik Tenants"), ("authentik.core", "authentik Core"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ], default="", help_text="Match events created by selected application. When left empty, all applications are matched.", diff --git a/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py b/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py index 0a311504e..22ab2314b 100644 --- a/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py +++ b/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py @@ -69,7 +69,7 @@ class Migration(migrations.Migration): "authentik Stages.OTP.Validate", ), ("authentik.stages.password", "authentik Stages.Password"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py b/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py index a57931b9c..dbf4e165c 100644 --- a/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py +++ b/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py @@ -73,7 +73,7 @@ class Migration(migrations.Migration): "authentik.stages.authenticator_webauthn", "authentik Stages.WebAuthn", ), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py b/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py index 99f0faecc..1003ee956 100644 --- a/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py +++ b/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py @@ -76,7 +76,7 @@ class Migration(migrations.Migration): "authentik Stages.Authenticator.WebAuthn", ), ("authentik.stages.password", "authentik Stages.Password"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py b/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py index 0701aaf20..553ea4319 100644 --- a/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py +++ b/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py @@ -76,7 +76,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_login", "authentik Stages.User Login"), ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/policies/event_matcher/migrations/0011_auto_20210302_0856.py b/authentik/policies/event_matcher/migrations/0011_auto_20210302_0856.py index 6d4d58cd8..c5c3710a0 100644 --- a/authentik/policies/event_matcher/migrations/0011_auto_20210302_0856.py +++ b/authentik/policies/event_matcher/migrations/0011_auto_20210302_0856.py @@ -77,7 +77,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_login", "authentik Stages.User Login"), ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/policies/event_matcher/migrations/0012_auto_20210323_1339.py b/authentik/policies/event_matcher/migrations/0012_auto_20210323_1339.py index d5a109d5b..2bb4fa839 100644 --- a/authentik/policies/event_matcher/migrations/0012_auto_20210323_1339.py +++ b/authentik/policies/event_matcher/migrations/0012_auto_20210323_1339.py @@ -74,7 +74,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.core", "authentik Core"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ], default="", help_text="Match events created by selected application. When left empty, all applications are matched.", diff --git a/authentik/policies/event_matcher/migrations/0013_alter_eventmatcherpolicy_app.py b/authentik/policies/event_matcher/migrations/0013_alter_eventmatcherpolicy_app.py index 46cc8443a..5ef2298b0 100644 --- a/authentik/policies/event_matcher/migrations/0013_alter_eventmatcherpolicy_app.py +++ b/authentik/policies/event_matcher/migrations/0013_alter_eventmatcherpolicy_app.py @@ -75,7 +75,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.core", "authentik Core"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ], default="", help_text="Match events created by selected application. When left empty, all applications are matched.", diff --git a/authentik/policies/event_matcher/migrations/0014_alter_eventmatcherpolicy_app.py b/authentik/policies/event_matcher/migrations/0014_alter_eventmatcherpolicy_app.py index c424c2b86..857a99937 100644 --- a/authentik/policies/event_matcher/migrations/0014_alter_eventmatcherpolicy_app.py +++ b/authentik/policies/event_matcher/migrations/0014_alter_eventmatcherpolicy_app.py @@ -76,7 +76,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.core", "authentik Core"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ], default="", help_text="Match events created by selected application. When left empty, all applications are matched.", diff --git a/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py b/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py index 4513d2b42..313a4de67 100644 --- a/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py +++ b/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py @@ -81,7 +81,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.tenants", "authentik Tenants"), ("authentik.core", "authentik Core"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ], default="", help_text="Match events created by selected application. When left empty, all applications are matched.", diff --git a/authentik/policies/event_matcher/migrations/0019_alter_eventmatcherpolicy_app.py b/authentik/policies/event_matcher/migrations/0019_alter_eventmatcherpolicy_app.py index fddc5bbab..b50b13711 100644 --- a/authentik/policies/event_matcher/migrations/0019_alter_eventmatcherpolicy_app.py +++ b/authentik/policies/event_matcher/migrations/0019_alter_eventmatcherpolicy_app.py @@ -69,7 +69,7 @@ class Migration(migrations.Migration): ("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.tenants", "authentik Tenants"), - ("authentik.managed", "authentik Managed"), + ("authentik.blueprints", "authentik Blueprints"), ("authentik.core", "authentik Core"), ], default="", diff --git a/authentik/providers/oauth2/managed.py b/authentik/providers/oauth2/managed.py index 9f4a3136a..4b073ec18 100644 --- a/authentik/providers/oauth2/managed.py +++ b/authentik/providers/oauth2/managed.py @@ -1,5 +1,5 @@ """OAuth2 Provider managed objects""" -from authentik.managed.manager import EnsureExists, ObjectManager +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.providers.oauth2.models import ScopeMapping SCOPE_OPENID_EXPRESSION = """ diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 43b80754e..f17c872af 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -24,6 +24,7 @@ from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction from authentik.events.utils import get_user from authentik.lib.generators import generate_id, generate_key +from authentik.lib.models import SerializerModel from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT @@ -335,7 +336,7 @@ class BaseGrantModel(models.Model): abstract = True -class AuthorizationCode(ExpiringModel, BaseGrantModel): +class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel): """OAuth2 Authorization Code""" code = models.CharField(max_length=255, unique=True, verbose_name=_("Code")) @@ -346,6 +347,12 @@ class AuthorizationCode(ExpiringModel, BaseGrantModel): max_length=255, null=True, verbose_name=_("Code Challenge Method") ) + @property + def serializer(self) -> Serializer: + from authentik.providers.oauth2.api.tokens import ExpiringBaseGrantModelSerializer + + return ExpiringBaseGrantModelSerializer + @property def c_hash(self): """https://openid.net/specs/openid-connect-core-1_0.html#IDToken""" @@ -398,13 +405,19 @@ class IDToken: return dic -class RefreshToken(ExpiringModel, BaseGrantModel): +class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel): """OAuth2 Refresh Token""" access_token = models.TextField(verbose_name=_("Access Token")) refresh_token = models.CharField(max_length=255, unique=True, verbose_name=_("Refresh Token")) _id_token = models.TextField(verbose_name=_("ID Token")) + @property + def serializer(self) -> Serializer: + from authentik.providers.oauth2.api.tokens import ExpiringBaseGrantModelSerializer + + return ExpiringBaseGrantModelSerializer + class Meta: verbose_name = _("OAuth2 Token") verbose_name_plural = _("OAuth2 Tokens") diff --git a/authentik/providers/oauth2/tests/test_token_cc.py b/authentik/providers/oauth2/tests/test_token_cc.py index 077eb0310..212e35dc1 100644 --- a/authentik/providers/oauth2/tests/test_token_cc.py +++ b/authentik/providers/oauth2/tests/test_token_cc.py @@ -5,10 +5,10 @@ from django.test import RequestFactory from django.urls import reverse from jwt import decode +from authentik.blueprints.manager import ObjectManager 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 -from authentik.managed.manager import ObjectManager from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.constants import ( GRANT_TYPE_CLIENT_CREDENTIALS, diff --git a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py index df83d8695..5e24dfba5 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py +++ b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py @@ -6,10 +6,10 @@ from django.test import RequestFactory from django.urls import reverse from jwt import decode +from authentik.blueprints.manager import ObjectManager 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 -from authentik.managed.manager import ObjectManager from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.constants import ( GRANT_TYPE_CLIENT_CREDENTIALS, diff --git a/authentik/providers/oauth2/tests/test_userinfo.py b/authentik/providers/oauth2/tests/test_userinfo.py index e4e756d79..cab69641c 100644 --- a/authentik/providers/oauth2/tests/test_userinfo.py +++ b/authentik/providers/oauth2/tests/test_userinfo.py @@ -4,11 +4,11 @@ from dataclasses import asdict from django.urls import reverse +from authentik.blueprints.manager import ObjectManager 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 from authentik.lib.generators import generate_id, generate_key -from authentik.managed.manager import ObjectManager from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken, ScopeMapping from authentik.providers.oauth2.tests.utils import OAuthTestCase diff --git a/authentik/providers/proxy/managed.py b/authentik/providers/proxy/managed.py index 2df4e03a3..ebfbad4bd 100644 --- a/authentik/providers/proxy/managed.py +++ b/authentik/providers/proxy/managed.py @@ -1,5 +1,5 @@ """OAuth2 Provider managed objects""" -from authentik.managed.manager import EnsureExists, ObjectManager +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.providers.oauth2.models import ScopeMapping from authentik.providers.proxy.models import SCOPE_AK_PROXY diff --git a/authentik/providers/saml/managed.py b/authentik/providers/saml/managed.py index 4023fccaa..2b5f284cc 100644 --- a/authentik/providers/saml/managed.py +++ b/authentik/providers/saml/managed.py @@ -1,5 +1,5 @@ """SAML Provider managed objects""" -from authentik.managed.manager import EnsureExists, ObjectManager +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.providers.saml.models import SAMLPropertyMapping GROUP_EXPRESSION = """ diff --git a/authentik/providers/saml/tests/test_auth_n_request.py b/authentik/providers/saml/tests/test_auth_n_request.py index 0d8250707..61112bf14 100644 --- a/authentik/providers/saml/tests/test_auth_n_request.py +++ b/authentik/providers/saml/tests/test_auth_n_request.py @@ -4,11 +4,11 @@ from base64 import b64encode from django.http.request import QueryDict from django.test import RequestFactory, TestCase +from authentik.blueprints.manager import ObjectManager 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 from authentik.lib.tests.utils import get_request -from authentik.managed.manager import ObjectManager from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.request_parser import AuthNRequestParser diff --git a/authentik/providers/saml/tests/test_schema.py b/authentik/providers/saml/tests/test_schema.py index ecdec2c6e..9d981d86c 100644 --- a/authentik/providers/saml/tests/test_schema.py +++ b/authentik/providers/saml/tests/test_schema.py @@ -4,10 +4,10 @@ from base64 import b64encode from django.test import RequestFactory, TestCase from lxml import etree # nosec +from authentik.blueprints.manager import ObjectManager 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 -from authentik.managed.manager import ObjectManager from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.request_parser import AuthNRequestParser diff --git a/authentik/root/celery.py b/authentik/root/celery.py index 925129112..05271dc9a 100644 --- a/authentik/root/celery.py +++ b/authentik/root/celery.py @@ -76,7 +76,7 @@ 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.managed.tasks import managed_reconcile + 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 diff --git a/authentik/root/settings.py b/authentik/root/settings.py index b00787467..ebfc4713f 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -122,7 +122,7 @@ INSTALLED_APPS = [ "authentik.stages.user_logout", "authentik.stages.user_write", "authentik.tenants", - "authentik.managed", + "authentik.blueprints", "rest_framework", "django_filters", "drf_spectacular", diff --git a/authentik/sources/ldap/managed.py b/authentik/sources/ldap/managed.py index 52758cb3a..98c4e383e 100644 --- a/authentik/sources/ldap/managed.py +++ b/authentik/sources/ldap/managed.py @@ -1,5 +1,5 @@ """LDAP Source managed objects""" -from authentik.managed.manager import EnsureExists, ObjectManager +from authentik.blueprints.manager import EnsureExists, ObjectManager from authentik.sources.ldap.models import LDAPPropertyMapping diff --git a/authentik/sources/ldap/tests/test_auth.py b/authentik/sources/ldap/tests/test_auth.py index 86e382a84..41429d82d 100644 --- a/authentik/sources/ldap/tests/test_auth.py +++ b/authentik/sources/ldap/tests/test_auth.py @@ -4,9 +4,9 @@ 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.core.models import User from authentik.lib.generators import generate_key -from authentik.managed.manager import ObjectManager from authentik.sources.ldap.auth import LDAPBackend from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.sync.users import UserLDAPSynchronizer diff --git a/authentik/sources/ldap/tests/test_sync.py b/authentik/sources/ldap/tests/test_sync.py index 0053adeea..5caa96e91 100644 --- a/authentik/sources/ldap/tests/test_sync.py +++ b/authentik/sources/ldap/tests/test_sync.py @@ -4,11 +4,11 @@ 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.core.models import Group, User from authentik.core.tests.utils import create_test_admin_user from authentik.events.models import Event, EventAction from authentik.lib.generators import generate_key -from authentik.managed.manager import ObjectManager from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index bda1c816b..947c943b2 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -211,6 +211,14 @@ class UserOAuthSourceConnection(UserSourceConnection): identifier = models.CharField(max_length=255) access_token = models.TextField(blank=True, null=True, default=None) + @property + def serializer(self) -> Serializer: + from authentik.sources.oauth.api.source_connection import ( + UserOAuthSourceConnectionSerializer, + ) + + return UserOAuthSourceConnectionSerializer + def save(self, *args, **kwargs): self.access_token = self.access_token or None super().save(*args, **kwargs) diff --git a/authentik/sources/plex/models.py b/authentik/sources/plex/models.py index 734f7fefa..40018bad5 100644 --- a/authentik/sources/plex/models.py +++ b/authentik/sources/plex/models.py @@ -7,7 +7,7 @@ from django.http.request import HttpRequest from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ from rest_framework.fields import CharField -from rest_framework.serializers import BaseSerializer +from rest_framework.serializers import BaseSerializer, Serializer from authentik.core.models import Source, UserSourceConnection from authentik.core.types import UILoginButton, UserSettingSerializer @@ -99,6 +99,12 @@ class PlexSourceConnection(UserSourceConnection): plex_token = models.TextField() identifier = models.TextField() + @property + def serializer(self) -> Serializer: + from authentik.sources.plex.api.source_connection import PlexSourceConnectionSerializer + + return PlexSourceConnectionSerializer + class Meta: verbose_name = _("User Plex Source Connection") diff --git a/authentik/stages/authenticator_duo/models.py b/authentik/stages/authenticator_duo/models.py index 6a4aaf7aa..663906474 100644 --- a/authentik/stages/authenticator_duo/models.py +++ b/authentik/stages/authenticator_duo/models.py @@ -7,11 +7,12 @@ from django.utils.translation import gettext_lazy as _ from django.views import View from django_otp.models import Device from duo_client.auth import Auth -from rest_framework.serializers import BaseSerializer +from rest_framework.serializers import BaseSerializer, Serializer from authentik import __version__ from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage +from authentik.lib.models import SerializerModel class AuthenticatorDuoStage(ConfigurableStage, Stage): @@ -65,7 +66,7 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage): verbose_name_plural = _("Duo Authenticator Setup Stages") -class DuoDevice(Device): +class DuoDevice(SerializerModel, Device): """Duo Device for a single user""" user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -73,9 +74,14 @@ class DuoDevice(Device): # Connect to the stage to when validating access we know the API Credentials stage = models.ForeignKey(AuthenticatorDuoStage, on_delete=models.CASCADE) duo_user_id = models.TextField() - last_t = models.DateTimeField(auto_now=True) + @property + def serializer(self) -> Serializer: + from authentik.stages.authenticator_duo.api import DuoDeviceSerializer + + return DuoDeviceSerializer + def __str__(self): return self.name or str(self.user) diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py index d66f4e7a4..d28445bd9 100644 --- a/authentik/stages/authenticator_sms/models.py +++ b/authentik/stages/authenticator_sms/models.py @@ -17,6 +17,7 @@ from twilio.rest import Client from authentik.core.types import UserSettingSerializer from authentik.events.models import Event, EventAction from authentik.flows.models import ConfigurableStage, Stage +from authentik.lib.models import SerializerModel from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.http import get_http_session @@ -163,7 +164,7 @@ def hash_phone_number(phone_number: str) -> str: return "hash:" + sha256(phone_number.encode()).hexdigest() -class SMSDevice(SideChannelDevice): +class SMSDevice(SerializerModel, SideChannelDevice): """SMS Device""" user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -184,6 +185,12 @@ class SMSDevice(SideChannelDevice): """Check if the phone number is hashed""" return self.phone_number.startswith("hash:") + @property + def serializer(self) -> BaseSerializer: + from authentik.stages.authenticator_sms.api import SMSDeviceSerializer + + return SMSDeviceSerializer + def verify_token(self, token): valid = super().verify_token(token) if valid: diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py index a6cb3a93a..06f9d1a7b 100644 --- a/authentik/stages/authenticator_webauthn/models.py +++ b/authentik/stages/authenticator_webauthn/models.py @@ -7,12 +7,13 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django.views import View from django_otp.models import Device -from rest_framework.serializers import BaseSerializer +from rest_framework.serializers import BaseSerializer, Serializer from webauthn.helpers.base64url_to_bytes import base64url_to_bytes from webauthn.helpers.structs import PublicKeyCredentialDescriptor from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage +from authentik.lib.models import SerializerModel class UserVerification(models.TextChoices): @@ -113,7 +114,7 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage): verbose_name_plural = _("WebAuthn Authenticator Setup Stages") -class WebAuthnDevice(Device): +class WebAuthnDevice(SerializerModel, Device): """WebAuthn Device for a single user""" user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -138,6 +139,12 @@ class WebAuthnDevice(Device): self.last_t = now() self.save() + @property + def serializer(self) -> Serializer: + from authentik.stages.authenticator_webauthn.api import WebAuthnDeviceSerializer + + return WebAuthnDeviceSerializer + def __str__(self): return self.name or str(self.user) diff --git a/authentik/stages/consent/models.py b/authentik/stages/consent/models.py index 672082147..086db4c3d 100644 --- a/authentik/stages/consent/models.py +++ b/authentik/stages/consent/models.py @@ -3,10 +3,11 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.views import View -from rest_framework.serializers import BaseSerializer +from rest_framework.serializers import BaseSerializer, Serializer from authentik.core.models import Application, ExpiringModel, User from authentik.flows.models import Stage +from authentik.lib.models import SerializerModel from authentik.lib.utils.time import timedelta_string_validator @@ -51,13 +52,19 @@ class ConsentStage(Stage): verbose_name_plural = _("Consent Stages") -class UserConsent(ExpiringModel): +class UserConsent(SerializerModel, ExpiringModel): """Consent given by a user for an application""" user = models.ForeignKey(User, on_delete=models.CASCADE) application = models.ForeignKey(Application, on_delete=models.CASCADE) permissions = models.TextField(default="") + @property + def serializer(self) -> Serializer: + from authentik.stages.consent.api import UserConsentSerializer + + return UserConsentSerializer + def __str__(self): return f"User Consent {self.application} by {self.user}" diff --git a/authentik/stages/invitation/models.py b/authentik/stages/invitation/models.py index 2805334e5..f9dbef49e 100644 --- a/authentik/stages/invitation/models.py +++ b/authentik/stages/invitation/models.py @@ -4,10 +4,11 @@ from uuid import uuid4 from django.db import models from django.utils.translation import gettext_lazy as _ from django.views import View -from rest_framework.serializers import BaseSerializer +from rest_framework.serializers import BaseSerializer, Serializer from authentik.core.models import ExpiringModel, User from authentik.flows.models import Stage +from authentik.lib.models import SerializerModel class InvitationStage(Stage): @@ -47,7 +48,7 @@ class InvitationStage(Stage): verbose_name_plural = _("Invitation Stages") -class Invitation(ExpiringModel): +class Invitation(SerializerModel, ExpiringModel): """Single-use invitation link""" invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -66,6 +67,12 @@ class Invitation(ExpiringModel): help_text=_("Optional fixed data to enforce on user enrollment."), ) + @property + def serializer(self) -> Serializer: + from authentik.stages.consent.api import UserConsentSerializer + + return UserConsentSerializer + def __str__(self): return f"Invitation {self.invite_uuid.hex} created by {self.created_by}" diff --git a/authentik/tenants/migrations/0002_default.py b/authentik/tenants/migrations/0002_default.py index b0e70ba9a..10576aba5 100644 --- a/authentik/tenants/migrations/0002_default.py +++ b/authentik/tenants/migrations/0002_default.py @@ -1,40 +1,12 @@ # Generated by Django 3.2.3 on 2021-05-29 16:55 -from django.apps.registry import Apps from django.db import migrations -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -def create_default_tenant(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): - Flow = apps.get_model("authentik_flows", "Flow") - Tenant = apps.get_model("authentik_tenants", "Tenant") - - db_alias = schema_editor.connection.alias - - default_authentication = ( - Flow.objects.using(db_alias).filter(slug="default-authentication-flow").first() - ) - default_invalidation = ( - Flow.objects.using(db_alias).filter(slug="default-invalidation-flow").first() - ) - - tenant, _ = Tenant.objects.using(db_alias).update_or_create( - domain="authentik-default", - default=True, - defaults={ - "flow_authentication": default_authentication, - "flow_invalidation": default_invalidation, - }, - ) class Migration(migrations.Migration): dependencies = [ ("authentik_tenants", "0001_initial"), - ("authentik_flows", "0008_default_flows"), ] - operations = [ - migrations.RunPython(create_default_tenant), - ] + operations = [] diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py index d56947ed0..f211b830d 100644 --- a/authentik/tenants/models.py +++ b/authentik/tenants/models.py @@ -3,13 +3,15 @@ from uuid import uuid4 from django.db import models from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer from authentik.crypto.models import CertificateKeyPair from authentik.flows.models import Flow +from authentik.lib.models import SerializerModel from authentik.lib.utils.time import timedelta_string_validator -class Tenant(models.Model): +class Tenant(SerializerModel): """Single tenant""" tenant_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -62,9 +64,14 @@ class Tenant(models.Model): on_delete=models.SET_DEFAULT, help_text=_(("Web Certificate used by the authentik Core webserver.")), ) - attributes = models.JSONField(default=dict, blank=True) + @property + def serializer(self) -> Serializer: + from authentik.tenants.api import TenantSerializer + + return TenantSerializer + @property def default_locale(self) -> str: """Get default locale""" diff --git a/blueprints/default/0-flow-password-change.yaml b/blueprints/default/0-flow-password-change.yaml new file mode 100644 index 000000000..6060b550f --- /dev/null +++ b/blueprints/default/0-flow-password-change.yaml @@ -0,0 +1,76 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: Change Password + policy_engine_mode: all + title: Change password + identifiers: + slug: default-password-change + model: authentik_flows.flow + id: flow +- attrs: + order: 300 + placeholder: Password + placeholder_expression: false + required: true + sub_text: '' + type: password + identifiers: + field_key: password + label: Password + id: prompt-field-password + model: authentik_stages_prompt.prompt +- attrs: + order: 301 + placeholder: Password (repeat) + placeholder_expression: false + required: true + sub_text: '' + type: password + identifiers: + field_key: password_repeat + label: Password (repeat) + id: prompt-field-password-repeat + model: authentik_stages_prompt.prompt +- attrs: + fields: + - !KeyOf prompt-field-password + - !KeyOf prompt-field-password-repeat + meta_model_name: authentik_stages_prompt.promptstage + validation_policies: [] + identifiers: + name: default-password-change-prompt + id: default-password-change-prompt + model: authentik_stages_prompt.promptstage +- attrs: + create_users_as_inactive: false + create_users_group: null + meta_model_name: authentik_stages_user_write.userwritestage + user_path_template: '' + identifiers: + name: default-password-change-write + id: default-password-change-write + model: authentik_stages_user_write.userwritestage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-password-change-prompt + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 1 + stage: !KeyOf default-password-change-write + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/10-flow-default-authentication-flow.yaml b/blueprints/default/10-flow-default-authentication-flow.yaml new file mode 100644 index 000000000..990241150 --- /dev/null +++ b/blueprints/default/10-flow-default-authentication-flow.yaml @@ -0,0 +1,102 @@ +entries: +- attrs: + cache_count: 1 + compatibility_mode: false + designation: authentication + layout: stacked + name: Welcome to authentik! + policy_engine_mode: all + title: Welcome to authentik! + identifiers: + slug: default-authentication-flow + model: authentik_flows.flow + id: flow +- attrs: + backends: + - authentik.core.auth.InbuiltBackend + - authentik.sources.ldap.auth.LDAPBackend + - authentik.core.auth.TokenBackend + - authentik.core.auth.TokenBackend + configure_flow: !Find [authentik_flows.flow, [slug, default-password-change]] + failed_attempts_before_cancel: 5 + meta_model_name: authentik_stages_password.passwordstage + identifiers: + name: default-authentication-password + id: default-authentication-password + model: authentik_stages_password.passwordstage +- attrs: + configuration_stages: [] + device_classes: + - static + - totp + - webauthn + - duo + - sms + last_auth_threshold: seconds=0 + meta_model_name: authentik_stages_authenticator_validate.authenticatorvalidatestage + not_configured_action: skip + identifiers: + name: default-authentication-mfa-validation + id: default-authentication-mfa-validation + model: authentik_stages_authenticator_validate.authenticatorvalidatestage +- attrs: + case_insensitive_matching: true + meta_model_name: authentik_stages_identification.identificationstage + show_matched_user: true + show_source_labels: false + sources: [] + user_fields: + - email + - username + identifiers: + name: default-authentication-identification + id: default-authentication-identification + model: authentik_stages_identification.identificationstage +- attrs: + meta_model_name: authentik_stages_user_login.userloginstage + session_duration: seconds=0 + identifiers: + name: default-authentication-login + id: default-authentication-login + model: authentik_stages_user_login.userloginstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 10 + stage: !KeyOf default-authentication-identification + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 20 + stage: !KeyOf default-authentication-password + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 30 + stage: !KeyOf default-authentication-mfa-validation + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 100 + stage: !KeyOf default-authentication-login + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/10-flow-default-invalidation-flow.yaml b/blueprints/default/10-flow-default-invalidation-flow.yaml new file mode 100644 index 000000000..6d67991b4 --- /dev/null +++ b/blueprints/default/10-flow-default-invalidation-flow.yaml @@ -0,0 +1,30 @@ +entries: +- attrs: + compatibility_mode: false + designation: invalidation + layout: stacked + name: Logout + policy_engine_mode: all + title: Default Invalidation Flow + identifiers: + pk: 46979d76-94d3-43b5-ad07-43e924c15d2c + slug: default-invalidation-flow + model: authentik_flows.flow + id: flow +- attrs: + meta_model_name: authentik_stages_user_logout.userlogoutstage + identifiers: + name: default-invalidation-logout + id: default-invalidation-logout + model: authentik_stages_user_logout.userlogoutstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-invalidation-logout + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/20-flow-default-authenticator-static-setup.yaml b/blueprints/default/20-flow-default-authenticator-static-setup.yaml new file mode 100644 index 000000000..95cc7a229 --- /dev/null +++ b/blueprints/default/20-flow-default-authenticator-static-setup.yaml @@ -0,0 +1,31 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: default-authenticator-static-setup + policy_engine_mode: any + title: Setup Static OTP Tokens + identifiers: + slug: default-authenticator-static-setup + model: authentik_flows.flow + id: flow +- attrs: + configure_flow: !KeyOf flow + meta_model_name: authentik_stages_authenticator_static.authenticatorstaticstage + token_count: 6 + identifiers: + name: default-authenticator-static-setup + id: default-authenticator-static-setup + model: authentik_stages_authenticator_static.authenticatorstaticstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-authenticator-static-setup + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/20-flow-default-authenticator-totp-setup.yaml b/blueprints/default/20-flow-default-authenticator-totp-setup.yaml new file mode 100644 index 000000000..80863fabb --- /dev/null +++ b/blueprints/default/20-flow-default-authenticator-totp-setup.yaml @@ -0,0 +1,31 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: default-authenticator-totp-setup + policy_engine_mode: any + title: Setup Two-Factor authentication + identifiers: + slug: default-authenticator-totp-setup + model: authentik_flows.flow + id: flow +- attrs: + configure_flow: !KeyOf flow + digits: 6 + meta_model_name: authentik_stages_authenticator_totp.authenticatortotpstage + identifiers: + name: default-authenticator-totp-setup + id: default-authenticator-totp-setup + model: authentik_stages_authenticator_totp.authenticatortotpstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-authenticator-totp-setup + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/20-flow-default-authenticator-webauthn-setup.yaml b/blueprints/default/20-flow-default-authenticator-webauthn-setup.yaml new file mode 100644 index 000000000..d27ac46e7 --- /dev/null +++ b/blueprints/default/20-flow-default-authenticator-webauthn-setup.yaml @@ -0,0 +1,33 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: default-authenticator-webauthn-setup + policy_engine_mode: any + title: Setup WebAuthn + identifiers: + slug: default-authenticator-webauthn-setup + model: authentik_flows.flow + id: flow +- attrs: + authenticator_attachment: null + configure_flow: !KeyOf flow + meta_model_name: authentik_stages_authenticator_webauthn.authenticatewebauthnstage + resident_key_requirement: preferred + user_verification: preferred + identifiers: + name: default-authenticator-webauthn-setup + id: default-authenticator-webauthn-setup + model: authentik_stages_authenticator_webauthn.authenticatewebauthnstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-authenticator-webauthn-setup + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml b/blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml new file mode 100644 index 000000000..87e3fa59b --- /dev/null +++ b/blueprints/default/20-flow-default-provider-authorization-explicit-consent.yaml @@ -0,0 +1,31 @@ +entries: +- attrs: + compatibility_mode: false + designation: authorization + layout: stacked + name: Authorize Application + policy_engine_mode: all + title: Redirecting to %(app)s + identifiers: + slug: default-provider-authorization-explicit-consent + model: authentik_flows.flow + id: flow +- attrs: + consent_expire_in: weeks=4 + meta_model_name: authentik_stages_consent.consentstage + mode: always_require + identifiers: + name: default-provider-authorization-consent + id: default-provider-authorization-consent + model: authentik_stages_consent.consentstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-provider-authorization-consent + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml b/blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml new file mode 100644 index 000000000..080010b52 --- /dev/null +++ b/blueprints/default/20-flow-default-provider-authorization-implicit-consent.yaml @@ -0,0 +1,12 @@ +entries: +- attrs: + compatibility_mode: false + designation: authorization + layout: stacked + name: Authorize Application + policy_engine_mode: all + title: Redirecting to %(app)s + identifiers: + slug: default-provider-authorization-implicit-consent + model: authentik_flows.flow +version: 1 diff --git a/blueprints/default/20-flow-default-source-authentication.yaml b/blueprints/default/20-flow-default-source-authentication.yaml new file mode 100644 index 000000000..198492532 --- /dev/null +++ b/blueprints/default/20-flow-default-source-authentication.yaml @@ -0,0 +1,50 @@ +entries: +- attrs: + compatibility_mode: false + designation: authentication + layout: stacked + name: Welcome to authentik! + policy_engine_mode: all + title: Welcome to authentik! + identifiers: + slug: default-source-authentication + model: authentik_flows.flow + id: flow +- attrs: + execution_logging: false + expression: | + # This policy ensures that this flow can only be used when the user + # is in a SSO Flow (meaning they come from an external IdP) + return ak_is_sso_flow + meta_model_name: authentik_policies_expression.expressionpolicy + identifiers: + name: default-source-authentication-if-sso + id: default-source-authentication-if-sso + model: authentik_policies_expression.expressionpolicy +- attrs: + meta_model_name: authentik_stages_user_login.userloginstage + session_duration: seconds=0 + identifiers: + name: default-source-authentication-login + id: default-source-authentication-login + model: authentik_stages_user_login.userloginstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 0 + stage: !KeyOf default-source-authentication-login + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + enabled: true + negate: false + timeout: 30 + identifiers: + order: 0 + policy: !KeyOf default-source-authentication-if-sso + target: !KeyOf flow + model: authentik_policies.policybinding +version: 1 diff --git a/blueprints/default/20-flow-default-source-enrollment.yaml b/blueprints/default/20-flow-default-source-enrollment.yaml new file mode 100644 index 000000000..248445271 --- /dev/null +++ b/blueprints/default/20-flow-default-source-enrollment.yaml @@ -0,0 +1,121 @@ +entries: +- attrs: + compatibility_mode: false + designation: enrollment + layout: stacked + name: Welcome to authentik! Please select a username. + policy_engine_mode: all + title: Welcome to authentik! Please select a username. + identifiers: + slug: default-source-enrollment + model: authentik_flows.flow + id: flow +- attrs: + order: 100 + placeholder: Username + placeholder_expression: false + required: true + sub_text: '' + type: text + identifiers: + field_key: username + label: Username + id: prompt-field-username + model: authentik_stages_prompt.prompt +- attrs: + execution_logging: false + 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'', {}) + meta_model_name: authentik_policies_expression.expressionpolicy + identifiers: + name: default-source-enrollment-if-username + id: default-source-enrollment-if-username + model: authentik_policies_expression.expressionpolicy +- attrs: + execution_logging: false + expression: | + # This policy ensures that this flow can only be used when the user + # is in a SSO Flow (meaning they come from an external IdP) + return ak_is_sso_flow + meta_model_name: authentik_policies_expression.expressionpolicy + identifiers: + name: default-source-enrollment-if-sso + id: default-source-enrollment-if-sso + model: authentik_policies_expression.expressionpolicy +- attrs: + meta_model_name: authentik_stages_user_login.userloginstage + session_duration: seconds=0 + identifiers: + name: default-source-enrollment-login + id: default-source-enrollment-login + model: authentik_stages_user_login.userloginstage +- attrs: + fields: + - !KeyOf prompt-field-username + meta_model_name: authentik_stages_prompt.promptstage + validation_policies: [] + identifiers: + name: default-source-enrollment-prompt + id: default-source-enrollment-prompt + model: authentik_stages_prompt.promptstage +- attrs: + create_users_as_inactive: false + create_users_group: null + meta_model_name: authentik_stages_user_write.userwritestage + user_path_template: '' + identifiers: + name: default-source-enrollment-write + id: default-source-enrollment-write + model: authentik_stages_user_write.userwritestage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: true + identifiers: + order: 0 + stage: !KeyOf default-source-enrollment-prompt + target: !KeyOf flow + id: prompt-binding + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 1 + stage: !KeyOf default-source-enrollment-write + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: all + re_evaluate_policies: false + identifiers: + order: 2 + stage: !KeyOf default-source-enrollment-login + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + enabled: true + negate: false + timeout: 30 + identifiers: + order: 0 + policy: !KeyOf default-source-enrollment-if-sso + target: !KeyOf flow + model: authentik_policies.policybinding +- attrs: + enabled: true + negate: false + timeout: 30 + identifiers: + order: 0 + policy: !KeyOf default-source-enrollment-if-username + target: !KeyOf prompt-binding + model: authentik_policies.policybinding +version: 1 diff --git a/blueprints/default/20-flow-default-source-pre-authentication.yaml b/blueprints/default/20-flow-default-source-pre-authentication.yaml new file mode 100644 index 000000000..a574cf0d9 --- /dev/null +++ b/blueprints/default/20-flow-default-source-pre-authentication.yaml @@ -0,0 +1,12 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: Pre-Authentication + policy_engine_mode: any + title: '' + identifiers: + slug: default-source-pre-authentication + model: authentik_flows.flow +version: 1 diff --git a/blueprints/default/30-flow-default-user-settings-flow.yaml b/blueprints/default/30-flow-default-user-settings-flow.yaml new file mode 100644 index 000000000..83bedffd6 --- /dev/null +++ b/blueprints/default/30-flow-default-user-settings-flow.yaml @@ -0,0 +1,157 @@ +entries: +- attrs: + compatibility_mode: false + designation: stage_configuration + layout: stacked + name: Update your info + policy_engine_mode: any + title: '' + identifiers: + slug: default-user-settings-flow + model: authentik_flows.flow + id: flow +- attrs: + order: 200 + placeholder: | + try: + return user.username + except: + return '' + placeholder_expression: true + required: true + sub_text: '' + type: text + identifiers: + field_key: username + label: Username + id: prompt-field-username + model: authentik_stages_prompt.prompt +- attrs: + order: 201 + placeholder: | + try: + return user.name + except: + return '' + placeholder_expression: true + required: true + sub_text: '' + type: text + identifiers: + field_key: name + label: Name + id: prompt-field-name + model: authentik_stages_prompt.prompt +- attrs: + order: 202 + placeholder: | + try: + return user.email + except: + return '' + placeholder_expression: true + required: true + sub_text: '' + type: email + identifiers: + field_key: email + label: Email + id: prompt-field-email + model: authentik_stages_prompt.prompt +- attrs: + order: 203 + placeholder: | + try: + return user.attributes.get("settings", {}).get("locale", "") + except: + return '' + placeholder_expression: true + required: true + sub_text: '' + type: ak-locale + identifiers: + field_key: attributes.settings.locale + label: Locale + id: prompt-field-locale + model: authentik_stages_prompt.prompt +- attrs: + execution_logging: false + expression: | + from authentik.lib.config import CONFIG + from authentik.core.models import ( + USER_ATTRIBUTE_CHANGE_EMAIL, + USER_ATTRIBUTE_CHANGE_NAME, + USER_ATTRIBUTE_CHANGE_USERNAME + ) + prompt_data = request.context.get("prompt_data") + + if not request.user.group_attributes(request.http_request).get( + USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True) + ): + if prompt_data.get("email") != request.user.email: + ak_message("Not allowed to change email address.") + return False + + if not request.user.group_attributes(request.http_request).get( + USER_ATTRIBUTE_CHANGE_NAME, CONFIG.y_bool("default_user_change_name", True) + ): + if prompt_data.get("name") != request.user.name: + ak_message("Not allowed to change name.") + return False + + if not request.user.group_attributes(request.http_request).get( + USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True) + ): + if prompt_data.get("username") != request.user.username: + ak_message("Not allowed to change username.") + return False + + return True + meta_model_name: authentik_policies_expression.expressionpolicy + name: default-user-settings-authorization + identifiers: + name: default-user-settings-authorization + model: authentik_policies_expression.expressionpolicy +- attrs: + create_users_as_inactive: false + create_users_group: null + meta_model_name: authentik_stages_user_write.userwritestage + user_path_template: '' + identifiers: + name: default-user-settings-write + id: default-user-settings-write + model: authentik_stages_user_write.userwritestage +- attrs: + fields: + - !KeyOf prompt-field-username + - !KeyOf prompt-field-name + - !KeyOf prompt-field-email + - !KeyOf prompt-field-locale + meta_model_name: authentik_stages_prompt.promptstage + validation_policies: + - !KeyOf default-user-settings-authorization + identifiers: + name: default-user-settings + id: default-user-settings + model: authentik_stages_prompt.promptstage +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 20 + stage: !KeyOf default-user-settings + target: !KeyOf flow + model: authentik_flows.flowstagebinding +- attrs: + evaluate_on_plan: true + invalid_response_action: retry + policy_engine_mode: any + re_evaluate_policies: false + identifiers: + order: 100 + stage: !KeyOf default-user-settings-write + target: !KeyOf flow + model: authentik_flows.flowstagebinding +version: 1 diff --git a/blueprints/default/90-default-tenant.yaml b/blueprints/default/90-default-tenant.yaml new file mode 100644 index 000000000..782d41081 --- /dev/null +++ b/blueprints/default/90-default-tenant.yaml @@ -0,0 +1,9 @@ +version: 1 +entries: +- attrs: + flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] + identifiers: + domain: authentik-default + default: True + model: authentik_tenants.Tenant diff --git a/website/static/flows/enrollment-2-stage.akflow b/blueprints/example/flows-enrollment-2-stage.yaml similarity index 63% rename from website/static/flows/enrollment-2-stage.akflow rename to blueprints/example/flows-enrollment-2-stage.yaml index 73bffb12a..af89ada4c 100644 --- a/website/static/flows/enrollment-2-stage.akflow +++ b/blueprints/example/flows-enrollment-2-stage.yaml @@ -1,119 +1,116 @@ version: 1 entries: - identifiers: - pk: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 slug: default-enrollment-flow model: authentik_flows.flow + id: flow attrs: name: Default enrollment Flow title: Welcome to authentik! designation: enrollment - identifiers: - pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 - model: authentik_stages_prompt.prompt - attrs: field_key: username label: Username + id: prompt-field-username + model: authentik_stages_prompt.prompt + attrs: type: username required: true placeholder: Username order: 0 - identifiers: - pk: 7db91ee8-4290-4e08-8d39-63f132402515 - model: authentik_stages_prompt.prompt - attrs: field_key: password label: Password + id: prompt-field-password + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password order: 0 - identifiers: - pk: d30b5eb4-7787-4072-b1ba-65b46e928920 - model: authentik_stages_prompt.prompt - attrs: field_key: password_repeat label: Password (repeat) + id: prompt-field-password-repeat + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password (repeat) order: 1 - identifiers: - pk: f78d977a-efa6-4cc2-9a0f-2621a9fd94d2 - model: authentik_stages_prompt.prompt - attrs: field_key: name label: Name + id: prompt-field-name + model: authentik_stages_prompt.prompt + attrs: type: text required: true placeholder: Name order: 0 - identifiers: - pk: 1ff91927-e33d-4615-95b0-c258e5f0df62 - model: authentik_stages_prompt.prompt - attrs: field_key: email label: Email + id: prompt-field-email + model: authentik_stages_prompt.prompt + attrs: type: email required: true placeholder: Email order: 1 - identifiers: - pk: 6c342b94-790d-425a-ae31-6196b6570722 name: default-enrollment-prompt-second + id: default-enrollment-prompt-second model: authentik_stages_prompt.promptstage attrs: fields: - - f78d977a-efa6-4cc2-9a0f-2621a9fd94d2 - - 1ff91927-e33d-4615-95b0-c258e5f0df62 + - !KeyOf prompt-field-name + - !KeyOf prompt-field-email - identifiers: - pk: 20375f30-7fa7-4562-8f6e-0f61889f2963 name: default-enrollment-prompt-first + id: default-enrollment-prompt-first model: authentik_stages_prompt.promptstage attrs: fields: - - cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 - - 7db91ee8-4290-4e08-8d39-63f132402515 - - d30b5eb4-7787-4072-b1ba-65b46e928920 + - !KeyOf prompt-field-username + - !KeyOf prompt-field-password + - !KeyOf prompt-field-password-repeat - identifiers: - pk: 77090897-eb3f-40db-81e6-b4074b1998c4 + pk: !KeyOf default-enrollment-user-login name: default-enrollment-user-login + id: default-enrollment-user-login model: authentik_stages_user_login.userloginstage attrs: session_duration: seconds=0 - identifiers: - pk: a4090add-f483-4ac6-8917-10b493ef843e name: default-enrollment-user-write + id: default-enrollment-user-write model: authentik_stages_user_write.userwritestage attrs: {} - identifiers: - pk: 34e1e7d5-8eed-4549-bc7a-305069ff7df0 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 20375f30-7fa7-4562-8f6e-0f61889f2963 + target: !KeyOf flow + stage: !KeyOf default-enrollment-prompt-first order: 10 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: e40467a6-3052-488c-a1b5-1ad7a80fe7b3 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 6c342b94-790d-425a-ae31-6196b6570722 + target: !KeyOf flow + stage: !KeyOf default-enrollment-prompt-second order: 11 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 76bc594e-2715-49ab-bd40-994abd9a7b70 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: a4090add-f483-4ac6-8917-10b493ef843e + target: !KeyOf flow + stage: !KeyOf default-enrollment-user-write order: 20 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 2f324f6d-7646-4108-a6e2-e7f90985477f - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 77090897-eb3f-40db-81e6-b4074b1998c4 + target: !KeyOf flow + stage: !KeyOf default-enrollment-user-login order: 100 model: authentik_flows.flowstagebinding attrs: diff --git a/website/static/flows/enrollment-email-verification.akflow b/blueprints/example/flows-enrollment-email-verification.yaml similarity index 65% rename from website/static/flows/enrollment-email-verification.akflow rename to blueprints/example/flows-enrollment-email-verification.yaml index 06b461a2a..add25827a 100644 --- a/website/static/flows/enrollment-email-verification.akflow +++ b/blueprints/example/flows-enrollment-email-verification.yaml @@ -1,66 +1,66 @@ version: 1 entries: - identifiers: - pk: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 slug: default-enrollment-flow + id: flow model: authentik_flows.flow attrs: name: Default enrollment Flow title: Welcome to authentik! designation: enrollment - identifiers: - pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 - model: authentik_stages_prompt.prompt - attrs: field_key: username label: Username + id: prompt-field-username + model: authentik_stages_prompt.prompt + attrs: type: username required: true placeholder: Username order: 0 - identifiers: - pk: 7db91ee8-4290-4e08-8d39-63f132402515 - model: authentik_stages_prompt.prompt - attrs: field_key: password label: Password + id: prompt-field-password + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password order: 0 - identifiers: - pk: d30b5eb4-7787-4072-b1ba-65b46e928920 - model: authentik_stages_prompt.prompt - attrs: field_key: password_repeat label: Password (repeat) + id: prompt-field-password-repeat + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password (repeat) order: 1 - identifiers: - pk: f78d977a-efa6-4cc2-9a0f-2621a9fd94d2 - model: authentik_stages_prompt.prompt - attrs: field_key: name label: Name + id: prompt-field-name + model: authentik_stages_prompt.prompt + attrs: type: text required: true placeholder: Name order: 0 - identifiers: - pk: 1ff91927-e33d-4615-95b0-c258e5f0df62 - model: authentik_stages_prompt.prompt - attrs: field_key: email label: Email + id: prompt-field-email + model: authentik_stages_prompt.prompt + attrs: type: email required: true placeholder: Email order: 1 - identifiers: - pk: 096e6282-6b30-4695-bd03-3b143eab5580 name: default-enrollment-email-verification + id: default-enrollment-email-verification model: authentik_stages_email.emailstage attrs: use_global_settings: true @@ -76,70 +76,65 @@ entries: template: email/account_confirmation.html activate_user_on_success: true - identifiers: - pk: 6c342b94-790d-425a-ae31-6196b6570722 name: default-enrollment-prompt-second + id: default-enrollment-prompt-second model: authentik_stages_prompt.promptstage attrs: fields: - - f78d977a-efa6-4cc2-9a0f-2621a9fd94d2 - - 1ff91927-e33d-4615-95b0-c258e5f0df62 + - !KeyOf prompt-field-name + - !KeyOf prompt-field-email - identifiers: - pk: 20375f30-7fa7-4562-8f6e-0f61889f2963 name: default-enrollment-prompt-first + id: default-enrollment-prompt-first model: authentik_stages_prompt.promptstage attrs: fields: - - cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 - - 7db91ee8-4290-4e08-8d39-63f132402515 - - d30b5eb4-7787-4072-b1ba-65b46e928920 + - !KeyOf prompt-field-username + - !KeyOf prompt-field-password + - !KeyOf prompt-field-password-repeat - identifiers: - pk: 77090897-eb3f-40db-81e6-b4074b1998c4 name: default-enrollment-user-login + id: default-enrollment-user-login model: authentik_stages_user_login.userloginstage attrs: session_duration: seconds=0 - identifiers: - pk: a4090add-f483-4ac6-8917-10b493ef843e name: default-enrollment-user-write + id: default-enrollment-user-write model: authentik_stages_user_write.userwritestage attrs: create_users_as_inactive: true - identifiers: - pk: 34e1e7d5-8eed-4549-bc7a-305069ff7df0 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 20375f30-7fa7-4562-8f6e-0f61889f2963 + target: !KeyOf flow + stage: !KeyOf default-enrollment-prompt-first order: 10 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: e40467a6-3052-488c-a1b5-1ad7a80fe7b3 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 6c342b94-790d-425a-ae31-6196b6570722 + target: !KeyOf flow + stage: !KeyOf default-enrollment-prompt-second order: 11 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 76bc594e-2715-49ab-bd40-994abd9a7b70 - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: a4090add-f483-4ac6-8917-10b493ef843e + target: !KeyOf flow + stage: !KeyOf default-enrollment-user-write order: 20 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 1db34a14-8985-4184-b5c9-254cd585d94f - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 096e6282-6b30-4695-bd03-3b143eab5580 + target: !KeyOf flow + stage: !KeyOf default-enrollment-email-verification order: 30 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 2f324f6d-7646-4108-a6e2-e7f90985477f - target: 773c6673-e4a2-423f-8d32-95b7b4a41cf3 - stage: 77090897-eb3f-40db-81e6-b4074b1998c4 + target: !KeyOf flow + stage: !KeyOf default-enrollment-user-login order: 40 model: authentik_flows.flowstagebinding attrs: diff --git a/website/static/flows/login-2fa.akflow b/blueprints/example/flows-login-2fa.yaml similarity index 64% rename from website/static/flows/login-2fa.akflow rename to blueprints/example/flows-login-2fa.yaml index 74f877f4a..c576a6e38 100644 --- a/website/static/flows/login-2fa.akflow +++ b/blueprints/example/flows-login-2fa.yaml @@ -2,29 +2,29 @@ version: 1 entries: - identifiers: slug: default-authentication-flow - pk: 563ece21-e9a4-47e5-a264-23ffd923e393 model: authentik_flows.flow + id: flow attrs: name: Default Authentication Flow title: Welcome to authentik! designation: authentication - identifiers: - pk: 7db93f1e-788b-4af6-8dc6-5cdeb59d8be7 + name: test-not-app-password + id: test-not-app-password model: authentik_policies_expression.expressionpolicy attrs: - name: test-not-app-password execution_logging: false - bound_to: 1 - expression: return context["auth_method"] != "app_password" + expression: | + return context["auth_method"] != "app_password" - identifiers: - pk: 69d41125-3987-499b-8d74-ef27b54b88c8 name: default-authentication-login + id: default-authentication-login model: authentik_stages_user_login.userloginstage attrs: session_duration: seconds=0 - identifiers: - pk: 5f594f27-0def-488d-9855-fe604eb13de5 name: default-authentication-identification + id: default-authentication-identification model: authentik_stages_identification.identificationstage attrs: user_fields: @@ -34,13 +34,14 @@ entries: enrollment_flow: null recovery_flow: null - identifiers: - pk: 37f709c3-8817-45e8-9a93-80a925d293c2 name: default-authentication-flow-mfa + id: default-authentication-flow-mfa model: authentik_stages_authenticator_validate.AuthenticatorValidateStage attrs: {} - identifiers: - pk: d8affa62-500c-4c5c-a01f-5835e1ffdf40 + pk: !KeyOf default-authentication-password name: default-authentication-password + id: default-authentication-password model: authentik_stages_password.passwordstage attrs: backends: @@ -48,44 +49,40 @@ entries: - authentik.core.auth.TokenBackend - authentik.sources.ldap.auth.LDAPBackend - identifiers: - pk: a3056482-b692-4e3a-93f1-7351c6a351c7 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: 5f594f27-0def-488d-9855-fe604eb13de5 + target: !KeyOf flow + stage: !KeyOf default-authentication-identification order: 10 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 4e8538cf-3e18-4a68-82ae-6df6725fa2e6 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: d8affa62-500c-4c5c-a01f-5835e1ffdf40 + target: !KeyOf flow + stage: !KeyOf default-authentication-password order: 20 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 688aec6f-5622-42c6-83a5-d22072d7e798 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: 37f709c3-8817-45e8-9a93-80a925d293c2 + target: !KeyOf flow + stage: !KeyOf default-authentication-flow-mfa order: 30 model: authentik_flows.flowstagebinding + id: flow-binding-mfa attrs: evaluate_on_plan: false re_evaluate_policies: true policy_engine_mode: any invalid_response_action: retry - identifiers: - pk: f3fede3a-a9b5-4232-9ec7-be7ff4194b27 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: 69d41125-3987-499b-8d74-ef27b54b88c8 + target: !KeyOf flow + stage: !KeyOf default-authentication-login order: 100 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 6e40ae4d-a4ed-4bd7-a784-27b1fe5859d2 - policy: 7db93f1e-788b-4af6-8dc6-5cdeb59d8be7 - target: 688aec6f-5622-42c6-83a5-d22072d7e798 + policy: !KeyOf test-not-app-password + target: !KeyOf flow-binding-mfa order: 0 model: authentik_policies.policybinding attrs: diff --git a/website/static/flows/login-conditional-captcha.akflow b/blueprints/example/flows-login-conditional-captcha.yaml similarity index 66% rename from website/static/flows/login-conditional-captcha.akflow rename to blueprints/example/flows-login-conditional-captcha.yaml index a6ac31a8c..7b110448f 100644 --- a/website/static/flows/login-conditional-captcha.akflow +++ b/blueprints/example/flows-login-conditional-captcha.yaml @@ -2,7 +2,7 @@ version: 1 entries: - identifiers: slug: default-authentication-flow - pk: 563ece21-e9a4-47e5-a264-23ffd923e393 + id: flow model: authentik_flows.flow attrs: name: Default Authentication Flow @@ -10,20 +10,20 @@ entries: designation: authentication - identifiers: name: default-authentication-login - pk: 69d41125-3987-499b-8d74-ef27b54b88c8 + id: default-authentication-login model: authentik_stages_user_login.userloginstage attrs: session_duration: seconds=0 - identifiers: name: default-authentication-flow-captcha - pk: a368cafc-1494-45e9-b75b-b5e7ac2bd3e4 + id: default-authentication-flow-captcha model: authentik_stages_captcha.captchastage attrs: public_key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI private_key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe - identifiers: name: default-authentication-identification - pk: 5f594f27-0def-488d-9855-fe604eb13de5 + id: default-authentication-identification model: authentik_stages_identification.identificationstage attrs: user_fields: @@ -34,7 +34,7 @@ entries: recovery_flow: null - identifiers: name: default-authentication-password - pk: d8affa62-500c-4c5c-a01f-5835e1ffdf40 + id: default-authentication-password model: authentik_stages_password.passwordstage attrs: backends: @@ -42,50 +42,46 @@ entries: - authentik.core.auth.TokenBackend - authentik.sources.ldap.auth.LDAPBackend - identifiers: - pk: a3056482-b692-4e3a-93f1-7351c6a351c7 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: 5f594f27-0def-488d-9855-fe604eb13de5 + target: !KeyOf flow + stage: !KeyOf default-authentication-identification order: 10 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 4e8538cf-3e18-4a68-82ae-6df6725fa2e6 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: d8affa62-500c-4c5c-a01f-5835e1ffdf40 + target: !KeyOf flow + stage: !KeyOf default-authentication-password order: 20 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 3bcd6af0-48a6-4e18-87f3-d251a1a58226 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: a368cafc-1494-45e9-b75b-b5e7ac2bd3e4 + target: !KeyOf flow + stage: !KeyOf default-authentication-flow-captcha order: 30 + id: flow-binding-captcha model: authentik_flows.flowstagebinding attrs: evaluate_on_plan: false re_evaluate_policies: true - identifiers: - pk: f3fede3a-a9b5-4232-9ec7-be7ff4194b27 - target: 563ece21-e9a4-47e5-a264-23ffd923e393 - stage: 69d41125-3987-499b-8d74-ef27b54b88c8 + target: !KeyOf flow + stage: !KeyOf default-authentication-login order: 100 model: authentik_flows.flowstagebinding attrs: re_evaluate_policies: false - identifiers: - pk: 688c9890-47ad-4327-a9e5-380e88d34be5 + name: default-authentication-flow-conditional-captcha + id: default-authentication-flow-conditional-captcha model: authentik_policies_reputation.reputationpolicy attrs: - name: default-authentication-flow-conditional-captcha check_ip: true check_username: true threshold: -5 - identifiers: - pk: 02e4d220-3448-44db-822e-c5255cf7c250 - policy: 688c9890-47ad-4327-a9e5-380e88d34be5 - target: 3bcd6af0-48a6-4e18-87f3-d251a1a58226 + policy: !KeyOf default-authentication-flow-conditional-captcha + target: !KeyOf flow-binding-captcha order: 0 model: authentik_policies.policybinding attrs: diff --git a/website/static/flows/recovery-email-verification.akflow b/blueprints/example/flows-recovery-email-verification.yaml similarity index 66% rename from website/static/flows/recovery-email-verification.akflow rename to blueprints/example/flows-recovery-email-verification.yaml index ab9786c2a..839d978dd 100644 --- a/website/static/flows/recovery-email-verification.akflow +++ b/blueprints/example/flows-recovery-email-verification.yaml @@ -1,8 +1,8 @@ version: 1 entries: - identifiers: - pk: a5993183-89c0-43d2-a7f4-ddffb17baba7 slug: default-recovery-flow + id: flow model: authentik_flows.flow attrs: name: Default recovery flow @@ -13,11 +13,11 @@ entries: compatibility_mode: false layout: stacked - identifiers: - pk: 7db91ee8-4290-4e08-8d39-63f132402515 - model: authentik_stages_prompt.prompt - attrs: field_key: password label: Password + id: prompt-field-password + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password @@ -25,11 +25,11 @@ entries: sub_text: "" placeholder_expression: false - identifiers: - pk: d30b5eb4-7787-4072-b1ba-65b46e928920 - model: authentik_stages_prompt.prompt - attrs: field_key: password_repeat label: Password (repeat) + id: prompt-field-password-repeat + model: authentik_stages_prompt.prompt + attrs: type: password required: true placeholder: Password (repeat) @@ -37,24 +37,16 @@ entries: sub_text: "" placeholder_expression: false - identifiers: - pk: 1c5709ae-1b3e-413a-a117-260ab509bf5c + name: default-recovery-skip-if-restored + id: default-recovery-skip-if-restored model: authentik_policies_expression.expressionpolicy attrs: - name: default-recovery-skip-if-restored execution_logging: false - bound_to: 2 - expression: return request.context.get('is_restored', False) + expression: | + return request.context.get('is_restored', False) - identifiers: - pk: 1c5709ae-1b3e-413a-a117-260ab509bf5c - model: authentik_policies_expression.expressionpolicy - attrs: - name: default-recovery-skip-if-restored - execution_logging: false - bound_to: 2 - expression: return request.context.get('is_restored', False) - - identifiers: - pk: 4ac5719f-32c0-441c-8a7e-33c5ea0db7da name: default-recovery-email + id: default-recovery-email model: authentik_stages_email.emailstage attrs: use_global_settings: true @@ -70,16 +62,16 @@ entries: template: email/password_reset.html activate_user_on_success: true - identifiers: - pk: 68b25ad5-318a-496e-95a7-cf4d94247f0d name: default-recovery-user-write + id: default-recovery-user-write model: authentik_stages_user_write.userwritestage attrs: create_users_as_inactive: false create_users_group: null user_path_template: "" - identifiers: - pk: 94843ef6-28fe-4939-bd61-cd46bb34f1de name: default-recovery-identification + id: default-recovery-identification model: authentik_stages_identification.identificationstage attrs: user_fields: @@ -94,37 +86,37 @@ entries: sources: [] show_source_labels: false - identifiers: - pk: e74230b2-82bc-4843-8b18-2c3a66a62d57 name: default-recovery-user-login + id: default-recovery-user-login model: authentik_stages_user_login.userloginstage attrs: session_duration: seconds=0 - identifiers: - pk: fa2d8d65-1809-4dcc-bdc0-56266e0f7971 name: Change your password + name: stages-prompt-password model: authentik_stages_prompt.promptstage attrs: fields: - - 7db91ee8-4290-4e08-8d39-63f132402515 - - d30b5eb4-7787-4072-b1ba-65b46e928920 + - !KeyOf prompt-field-password + - !KeyOf prompt-field-password-repeat validation_policies: [] - identifiers: - pk: 7af7558e-2196-4b9f-a08e-d38420b7cfbb - target: a5993183-89c0-43d2-a7f4-ddffb17baba7 - stage: 94843ef6-28fe-4939-bd61-cd46bb34f1de + target: !KeyOf flow + stage: !KeyOf default-recovery-identification order: 10 model: authentik_flows.flowstagebinding + id: flow-binding-identification attrs: evaluate_on_plan: true re_evaluate_policies: true policy_engine_mode: any invalid_response_action: retry - identifiers: - pk: 29446fd6-dd93-4e92-9830-2d81debad5ae - target: a5993183-89c0-43d2-a7f4-ddffb17baba7 - stage: 4ac5719f-32c0-441c-8a7e-33c5ea0db7da + target: !KeyOf flow + stage: !KeyOf default-recovery-email order: 20 model: authentik_flows.flowstagebinding + id: flow-binding-email attrs: evaluate_on_plan: true re_evaluate_policies: true @@ -132,8 +124,8 @@ entries: invalid_response_action: retry - identifiers: pk: 1219d06e-2c06-4c5b-a162-78e3959c6cf0 - target: a5993183-89c0-43d2-a7f4-ddffb17baba7 - stage: fa2d8d65-1809-4dcc-bdc0-56266e0f7971 + target: !KeyOf flow + stage: !KeyOf stages-prompt-password order: 30 model: authentik_flows.flowstagebinding attrs: @@ -142,9 +134,8 @@ entries: policy_engine_mode: any invalid_response_action: retry - identifiers: - pk: 66de86ba-0707-46a0-8475-ff2e260d6935 - target: a5993183-89c0-43d2-a7f4-ddffb17baba7 - stage: 68b25ad5-318a-496e-95a7-cf4d94247f0d + target: !KeyOf flow + stage: !KeyOf default-recovery-user-write order: 40 model: authentik_flows.flowstagebinding attrs: @@ -153,9 +144,8 @@ entries: policy_engine_mode: any invalid_response_action: retry - identifiers: - pk: 9cec2334-d4a2-4895-a2b2-bc5ae4e9639a - target: a5993183-89c0-43d2-a7f4-ddffb17baba7 - stage: e74230b2-82bc-4843-8b18-2c3a66a62d57 + target: !KeyOf flow + stage: !KeyOf default-recovery-user-login order: 100 model: authentik_flows.flowstagebinding attrs: @@ -164,9 +154,8 @@ entries: policy_engine_mode: any invalid_response_action: retry - identifiers: - pk: 95aad215-8729-4177-953d-41ffbe86239e - policy: 1c5709ae-1b3e-413a-a117-260ab509bf5c - target: 7af7558e-2196-4b9f-a08e-d38420b7cfbb + policy: !KeyOf default-recovery-skip-if-restored + target: !KeyOf flow-binding-identification order: 0 model: authentik_policies.policybinding attrs: @@ -174,9 +163,8 @@ entries: enabled: true timeout: 30 - identifiers: - pk: a5454cbc-d2e4-403a-84af-6af999990b12 - policy: 1c5709ae-1b3e-413a-a117-260ab509bf5c - target: 29446fd6-dd93-4e92-9830-2d81debad5ae + policy: !KeyOf default-recovery-skip-if-restored + target: !KeyOf flow-binding-email order: 0 model: authentik_policies.policybinding attrs: diff --git a/website/static/flows/unenrollment.akflow b/blueprints/example/flows-unenrollment.yaml similarity index 66% rename from website/static/flows/unenrollment.akflow rename to blueprints/example/flows-unenrollment.yaml index efaa673ab..c843570e1 100644 --- a/website/static/flows/unenrollment.akflow +++ b/blueprints/example/flows-unenrollment.yaml @@ -1,22 +1,21 @@ version: 1 entries: - identifiers: - pk: 59a576ce-2f23-4a63-b63a-d18dc7e550f5 slug: default-unenrollment-flow model: authentik_flows.flow + id: flow attrs: name: Default unenrollment flow title: Delete your account designation: unenrollment - identifiers: - pk: c62ac2a4-2735-4a0f-abd0-8523d68c1209 name: default-unenrollment-user-delete + id: default-unenrollment-user-delete model: authentik_stages_user_delete.userdeletestage attrs: {} - identifiers: - pk: eb9aff2b-b95d-40b3-ad08-233aa77bbcf3 - target: 59a576ce-2f23-4a63-b63a-d18dc7e550f5 - stage: c62ac2a4-2735-4a0f-abd0-8523d68c1209 + target: !KeyOf flow + stage: !KeyOf default-unenrollment-user-delete order: 10 model: authentik_flows.flowstagebinding attrs: diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index c03637097..9166b1c10 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-07-28 19:11+0000\n" +"POT-Creation-Date: 2022-07-31 14:22+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -31,6 +31,18 @@ msgstr "" msgid "Validation Error" msgstr "" +#: authentik/blueprints/models.py:18 +msgid "Managed by authentik" +msgstr "" + +#: authentik/blueprints/models.py:69 +msgid "Blueprint Instance" +msgstr "" + +#: authentik/blueprints/models.py:70 +msgid "Blueprint Instances" +msgstr "" + #: authentik/core/api/providers.py:89 msgid "SAML Provider from Metadata" msgstr "" @@ -55,95 +67,95 @@ msgstr "" msgid "Users added to this group will be superusers." msgstr "" -#: authentik/core/models.py:146 +#: authentik/core/models.py:152 msgid "User's display name." msgstr "" -#: authentik/core/models.py:239 authentik/providers/oauth2/models.py:321 +#: authentik/core/models.py:251 authentik/providers/oauth2/models.py:322 msgid "User" msgstr "" -#: authentik/core/models.py:240 +#: authentik/core/models.py:252 msgid "Users" msgstr "" -#: authentik/core/models.py:251 +#: authentik/core/models.py:263 msgid "Flow used when authorizing this provider." msgstr "" -#: authentik/core/models.py:284 +#: authentik/core/models.py:296 msgid "Application's display Name." msgstr "" -#: authentik/core/models.py:285 +#: authentik/core/models.py:297 msgid "Internal application name, used in URLs." msgstr "" -#: authentik/core/models.py:297 +#: authentik/core/models.py:309 msgid "Open launch URL in a new browser tab or window." msgstr "" -#: authentik/core/models.py:356 +#: authentik/core/models.py:374 msgid "Application" msgstr "" -#: authentik/core/models.py:357 +#: authentik/core/models.py:375 msgid "Applications" msgstr "" -#: authentik/core/models.py:363 +#: authentik/core/models.py:381 msgid "Use the source-specific identifier" msgstr "" -#: authentik/core/models.py:371 +#: authentik/core/models.py:389 msgid "" "Use the user's email address, but deny enrollment when the email address " "already exists." msgstr "" -#: authentik/core/models.py:380 +#: authentik/core/models.py:398 msgid "" "Use the user's username, but deny enrollment when the username already " "exists." msgstr "" -#: authentik/core/models.py:387 +#: authentik/core/models.py:405 msgid "Source's display Name." msgstr "" -#: authentik/core/models.py:388 +#: authentik/core/models.py:406 msgid "Internal source name, used in URLs." msgstr "" -#: authentik/core/models.py:401 +#: authentik/core/models.py:419 msgid "Flow to use when authenticating existing users." msgstr "" -#: authentik/core/models.py:410 +#: authentik/core/models.py:428 msgid "Flow to use when enrolling new users." msgstr "" -#: authentik/core/models.py:560 +#: authentik/core/models.py:589 msgid "Token" msgstr "" -#: authentik/core/models.py:561 +#: authentik/core/models.py:590 msgid "Tokens" msgstr "" -#: authentik/core/models.py:604 +#: authentik/core/models.py:633 msgid "Property Mapping" msgstr "" -#: authentik/core/models.py:605 +#: authentik/core/models.py:634 msgid "Property Mappings" msgstr "" -#: authentik/core/models.py:641 +#: authentik/core/models.py:670 msgid "Authenticated Session" msgstr "" -#: authentik/core/models.py:642 +#: authentik/core/models.py:671 msgid "Authenticated Sessions" msgstr "" @@ -166,12 +178,12 @@ msgstr "" msgid "Go to home" msgstr "" -#: authentik/core/templates/if/admin.html:18 #: authentik/core/templates/if/admin.html:24 -#: authentik/core/templates/if/flow.html:37 -#: authentik/core/templates/if/flow.html:43 -#: authentik/core/templates/if/user.html:18 +#: authentik/core/templates/if/admin.html:30 +#: authentik/core/templates/if/flow.html:38 +#: authentik/core/templates/if/flow.html:44 #: authentik/core/templates/if/user.html:24 +#: authentik/core/templates/if/user.html:30 msgid "Loading..." msgstr "" @@ -227,21 +239,21 @@ msgstr "" msgid "Subject-alt name" msgstr "" -#: authentik/crypto/models.py:34 +#: authentik/crypto/models.py:35 msgid "PEM-encoded Certificate data" msgstr "" -#: authentik/crypto/models.py:37 +#: authentik/crypto/models.py:38 msgid "" "Optional Private Key. If this is set, you can use this keypair for " "encryption." msgstr "" -#: authentik/crypto/models.py:100 +#: authentik/crypto/models.py:107 msgid "Certificate-Key Pair" msgstr "" -#: authentik/crypto/models.py:101 +#: authentik/crypto/models.py:108 msgid "Certificate-Key Pairs" msgstr "" @@ -250,89 +262,89 @@ msgstr "" msgid "Successfully imported %(count)d files." msgstr "" -#: authentik/events/models.py:288 +#: authentik/events/models.py:294 msgid "Event" msgstr "" -#: authentik/events/models.py:289 +#: authentik/events/models.py:295 msgid "Events" msgstr "" -#: authentik/events/models.py:295 +#: authentik/events/models.py:301 msgid "authentik inbuilt notifications" msgstr "" -#: authentik/events/models.py:296 +#: authentik/events/models.py:302 msgid "Generic Webhook" msgstr "" -#: authentik/events/models.py:297 +#: authentik/events/models.py:303 msgid "Slack Webhook (Slack/Discord)" msgstr "" -#: authentik/events/models.py:298 +#: authentik/events/models.py:304 msgid "Email" msgstr "" -#: authentik/events/models.py:316 +#: authentik/events/models.py:322 msgid "" "Only send notification once, for example when sending a webhook into a chat " "channel." msgstr "" -#: authentik/events/models.py:374 +#: authentik/events/models.py:380 msgid "Severity" msgstr "" -#: authentik/events/models.py:379 +#: authentik/events/models.py:385 msgid "Dispatched for user" msgstr "" -#: authentik/events/models.py:456 +#: authentik/events/models.py:468 msgid "Notification Transport" msgstr "" -#: authentik/events/models.py:457 +#: authentik/events/models.py:469 msgid "Notification Transports" msgstr "" -#: authentik/events/models.py:463 +#: authentik/events/models.py:475 msgid "Notice" msgstr "" -#: authentik/events/models.py:464 +#: authentik/events/models.py:476 msgid "Warning" msgstr "" -#: authentik/events/models.py:465 +#: authentik/events/models.py:477 msgid "Alert" msgstr "" -#: authentik/events/models.py:485 +#: authentik/events/models.py:503 msgid "Notification" msgstr "" -#: authentik/events/models.py:486 +#: authentik/events/models.py:504 msgid "Notifications" msgstr "" -#: authentik/events/models.py:506 +#: authentik/events/models.py:524 msgid "Controls which severity level the created notifications will have." msgstr "" -#: authentik/events/models.py:526 +#: authentik/events/models.py:550 msgid "Notification Rule" msgstr "" -#: authentik/events/models.py:527 +#: authentik/events/models.py:551 msgid "Notification Rules" msgstr "" -#: authentik/events/models.py:548 +#: authentik/events/models.py:572 msgid "Notification Webhook Mapping" msgstr "" -#: authentik/events/models.py:549 +#: authentik/events/models.py:573 msgid "Notification Webhook Mappings" msgstr "" @@ -430,10 +442,6 @@ msgstr "" msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'." msgstr "" -#: authentik/managed/models.py:12 -msgid "Managed by authentik" -msgstr "" - #: authentik/outposts/api/service_connections.py:132 msgid "" "You can only use an empty kubeconfig when connecting to a local cluster." @@ -443,33 +451,33 @@ msgstr "" msgid "Invalid kubeconfig" msgstr "" -#: authentik/outposts/models.py:154 +#: authentik/outposts/models.py:155 msgid "Outpost Service-Connection" msgstr "" -#: authentik/outposts/models.py:155 +#: authentik/outposts/models.py:156 msgid "Outpost Service-Connections" msgstr "" -#: authentik/outposts/models.py:191 +#: authentik/outposts/models.py:192 msgid "" "Certificate/Key used for authentication. Can be left empty for no " "authentication." msgstr "" -#: authentik/outposts/models.py:204 +#: authentik/outposts/models.py:211 msgid "Docker Service-Connection" msgstr "" -#: authentik/outposts/models.py:205 +#: authentik/outposts/models.py:212 msgid "Docker Service-Connections" msgstr "" -#: authentik/outposts/models.py:230 +#: authentik/outposts/models.py:243 msgid "Kubernetes Service-Connection" msgstr "" -#: authentik/outposts/models.py:231 +#: authentik/outposts/models.py:244 msgid "Kubernetes Service-Connections" msgstr "" @@ -666,184 +674,184 @@ msgstr "" msgid "LDAP Providers" msgstr "" -#: authentik/providers/oauth2/models.py:37 +#: authentik/providers/oauth2/models.py:38 msgid "Confidential" msgstr "" -#: authentik/providers/oauth2/models.py:38 +#: authentik/providers/oauth2/models.py:39 msgid "Public" msgstr "" -#: authentik/providers/oauth2/models.py:60 +#: authentik/providers/oauth2/models.py:61 msgid "Based on the Hashed User ID" msgstr "" -#: authentik/providers/oauth2/models.py:61 +#: authentik/providers/oauth2/models.py:62 msgid "Based on the username" msgstr "" -#: authentik/providers/oauth2/models.py:64 +#: authentik/providers/oauth2/models.py:65 msgid "Based on the User's Email. This is recommended over the UPN method." msgstr "" -#: authentik/providers/oauth2/models.py:80 +#: authentik/providers/oauth2/models.py:81 msgid "Same identifier is used for all providers" msgstr "" -#: authentik/providers/oauth2/models.py:82 +#: authentik/providers/oauth2/models.py:83 msgid "Each provider has a different issuer, based on the application slug." msgstr "" -#: authentik/providers/oauth2/models.py:89 +#: authentik/providers/oauth2/models.py:90 msgid "code (Authorization Code Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:90 +#: authentik/providers/oauth2/models.py:91 msgid "id_token (Implicit Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:91 +#: authentik/providers/oauth2/models.py:92 msgid "id_token token (Implicit Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:92 +#: authentik/providers/oauth2/models.py:93 msgid "code token (Hybrid Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:93 +#: authentik/providers/oauth2/models.py:94 msgid "code id_token (Hybrid Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:94 +#: authentik/providers/oauth2/models.py:95 msgid "code id_token token (Hybrid Flow)" msgstr "" -#: authentik/providers/oauth2/models.py:100 +#: authentik/providers/oauth2/models.py:101 msgid "HS256 (Symmetric Encryption)" msgstr "" -#: authentik/providers/oauth2/models.py:101 +#: authentik/providers/oauth2/models.py:102 msgid "RS256 (Asymmetric Encryption)" msgstr "" -#: authentik/providers/oauth2/models.py:102 +#: authentik/providers/oauth2/models.py:103 msgid "ES256 (Asymmetric Encryption)" msgstr "" -#: authentik/providers/oauth2/models.py:108 +#: authentik/providers/oauth2/models.py:109 msgid "Scope used by the client" msgstr "" -#: authentik/providers/oauth2/models.py:134 +#: authentik/providers/oauth2/models.py:135 msgid "Scope Mapping" msgstr "" -#: authentik/providers/oauth2/models.py:135 +#: authentik/providers/oauth2/models.py:136 msgid "Scope Mappings" msgstr "" -#: authentik/providers/oauth2/models.py:145 +#: authentik/providers/oauth2/models.py:146 msgid "Client Type" msgstr "" -#: authentik/providers/oauth2/models.py:147 +#: authentik/providers/oauth2/models.py:148 msgid "" "Confidential clients are capable of maintaining the confidentiality of their " "credentials. Public clients are incapable" msgstr "" -#: authentik/providers/oauth2/models.py:154 +#: authentik/providers/oauth2/models.py:155 msgid "Client ID" msgstr "" -#: authentik/providers/oauth2/models.py:160 +#: authentik/providers/oauth2/models.py:161 msgid "Client Secret" msgstr "" -#: authentik/providers/oauth2/models.py:166 +#: authentik/providers/oauth2/models.py:167 msgid "Redirect URIs" msgstr "" -#: authentik/providers/oauth2/models.py:167 +#: authentik/providers/oauth2/models.py:168 msgid "Enter each URI on a new line." msgstr "" -#: authentik/providers/oauth2/models.py:172 +#: authentik/providers/oauth2/models.py:173 msgid "Include claims in id_token" msgstr "" -#: authentik/providers/oauth2/models.py:220 +#: authentik/providers/oauth2/models.py:221 msgid "Signing Key" msgstr "" -#: authentik/providers/oauth2/models.py:224 +#: authentik/providers/oauth2/models.py:225 msgid "" "Key used to sign the tokens. Only required when JWT Algorithm is set to " "RS256." msgstr "" -#: authentik/providers/oauth2/models.py:231 +#: authentik/providers/oauth2/models.py:232 msgid "" "Any JWT signed by the JWK of the selected source can be used to authenticate." msgstr "" -#: authentik/providers/oauth2/models.py:313 +#: authentik/providers/oauth2/models.py:314 msgid "OAuth2/OpenID Provider" msgstr "" -#: authentik/providers/oauth2/models.py:314 +#: authentik/providers/oauth2/models.py:315 msgid "OAuth2/OpenID Providers" msgstr "" -#: authentik/providers/oauth2/models.py:322 +#: authentik/providers/oauth2/models.py:323 msgid "Scopes" msgstr "" -#: authentik/providers/oauth2/models.py:341 +#: authentik/providers/oauth2/models.py:342 msgid "Code" msgstr "" -#: authentik/providers/oauth2/models.py:342 +#: authentik/providers/oauth2/models.py:343 msgid "Nonce" msgstr "" -#: authentik/providers/oauth2/models.py:343 +#: authentik/providers/oauth2/models.py:344 msgid "Is Authentication?" msgstr "" -#: authentik/providers/oauth2/models.py:344 +#: authentik/providers/oauth2/models.py:345 msgid "Code Challenge" msgstr "" -#: authentik/providers/oauth2/models.py:346 +#: authentik/providers/oauth2/models.py:347 msgid "Code Challenge Method" msgstr "" -#: authentik/providers/oauth2/models.py:360 +#: authentik/providers/oauth2/models.py:367 msgid "Authorization Code" msgstr "" -#: authentik/providers/oauth2/models.py:361 +#: authentik/providers/oauth2/models.py:368 msgid "Authorization Codes" msgstr "" -#: authentik/providers/oauth2/models.py:404 +#: authentik/providers/oauth2/models.py:411 msgid "Access Token" msgstr "" -#: authentik/providers/oauth2/models.py:405 +#: authentik/providers/oauth2/models.py:412 msgid "Refresh Token" msgstr "" -#: authentik/providers/oauth2/models.py:406 +#: authentik/providers/oauth2/models.py:413 msgid "ID Token" msgstr "" -#: authentik/providers/oauth2/models.py:409 +#: authentik/providers/oauth2/models.py:422 msgid "OAuth2 Token" msgstr "" -#: authentik/providers/oauth2/models.py:410 +#: authentik/providers/oauth2/models.py:423 msgid "OAuth2 Tokens" msgstr "" @@ -870,42 +878,42 @@ msgstr "" msgid "authentik API Access on behalf of your user" msgstr "" -#: authentik/providers/proxy/models.py:47 +#: authentik/providers/proxy/models.py:54 msgid "Validate SSL Certificates of upstream servers" msgstr "" -#: authentik/providers/proxy/models.py:48 +#: authentik/providers/proxy/models.py:55 msgid "Internal host SSL Validation" msgstr "" -#: authentik/providers/proxy/models.py:54 +#: authentik/providers/proxy/models.py:61 msgid "" "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive " "with internal_host." msgstr "" -#: authentik/providers/proxy/models.py:72 +#: authentik/providers/proxy/models.py:79 msgid "Set HTTP-Basic Authentication" msgstr "" -#: authentik/providers/proxy/models.py:74 +#: authentik/providers/proxy/models.py:81 msgid "" "Set a custom HTTP-Basic Authentication header based on values from authentik." msgstr "" -#: authentik/providers/proxy/models.py:79 +#: authentik/providers/proxy/models.py:86 msgid "HTTP-Basic Username Key" msgstr "" -#: authentik/providers/proxy/models.py:89 +#: authentik/providers/proxy/models.py:96 msgid "HTTP-Basic Password Key" msgstr "" -#: authentik/providers/proxy/models.py:144 +#: authentik/providers/proxy/models.py:151 msgid "Proxy Provider" msgstr "" -#: authentik/providers/proxy/models.py:145 +#: authentik/providers/proxy/models.py:152 msgid "Proxy Providers" msgstr "" @@ -1213,11 +1221,11 @@ msgstr "" msgid "Okta OAuth Sources" msgstr "" -#: authentik/sources/oauth/models.py:220 +#: authentik/sources/oauth/models.py:228 msgid "User OAuth Source Connection" msgstr "" -#: authentik/sources/oauth/models.py:221 +#: authentik/sources/oauth/models.py:229 msgid "User OAuth Source Connections" msgstr "" @@ -1245,11 +1253,11 @@ msgstr "" msgid "Plex Sources" msgstr "" -#: authentik/sources/plex/models.py:104 +#: authentik/sources/plex/models.py:110 msgid "User Plex Source Connection" msgstr "" -#: authentik/sources/plex/models.py:105 +#: authentik/sources/plex/models.py:111 msgid "User Plex Source Connections" msgstr "" @@ -1322,42 +1330,42 @@ msgstr "" msgid "SAML Sources" msgstr "" -#: authentik/stages/authenticator_duo/models.py:64 +#: authentik/stages/authenticator_duo/models.py:65 msgid "Duo Authenticator Setup Stage" msgstr "" -#: authentik/stages/authenticator_duo/models.py:65 +#: authentik/stages/authenticator_duo/models.py:66 msgid "Duo Authenticator Setup Stages" msgstr "" -#: authentik/stages/authenticator_duo/models.py:84 +#: authentik/stages/authenticator_duo/models.py:90 msgid "Duo Device" msgstr "" -#: authentik/stages/authenticator_duo/models.py:85 +#: authentik/stages/authenticator_duo/models.py:91 msgid "Duo Devices" msgstr "" -#: authentik/stages/authenticator_sms/models.py:53 +#: authentik/stages/authenticator_sms/models.py:56 msgid "" "When enabled, the Phone number is only used during enrollment to verify the " "users authenticity. Only a hash of the phone number is saved to ensure it is " "not re-used in the future." msgstr "" -#: authentik/stages/authenticator_sms/models.py:167 +#: authentik/stages/authenticator_sms/models.py:158 msgid "SMS Authenticator Setup Stage" msgstr "" -#: authentik/stages/authenticator_sms/models.py:168 +#: authentik/stages/authenticator_sms/models.py:159 msgid "SMS Authenticator Setup Stages" msgstr "" -#: authentik/stages/authenticator_sms/models.py:207 +#: authentik/stages/authenticator_sms/models.py:204 msgid "SMS Device" msgstr "" -#: authentik/stages/authenticator_sms/models.py:208 +#: authentik/stages/authenticator_sms/models.py:205 msgid "SMS Devices" msgstr "" @@ -1431,19 +1439,19 @@ msgstr "" msgid "Authenticator Validation Stages" msgstr "" -#: authentik/stages/authenticator_webauthn/models.py:112 +#: authentik/stages/authenticator_webauthn/models.py:113 msgid "WebAuthn Authenticator Setup Stage" msgstr "" -#: authentik/stages/authenticator_webauthn/models.py:113 +#: authentik/stages/authenticator_webauthn/models.py:114 msgid "WebAuthn Authenticator Setup Stages" msgstr "" -#: authentik/stages/authenticator_webauthn/models.py:146 +#: authentik/stages/authenticator_webauthn/models.py:153 msgid "WebAuthn Device" msgstr "" -#: authentik/stages/authenticator_webauthn/models.py:147 +#: authentik/stages/authenticator_webauthn/models.py:154 msgid "WebAuthn Devices" msgstr "" @@ -1465,19 +1473,19 @@ msgstr "" msgid "Captcha Stages" msgstr "" -#: authentik/stages/consent/models.py:50 +#: authentik/stages/consent/models.py:51 msgid "Consent Stage" msgstr "" -#: authentik/stages/consent/models.py:51 +#: authentik/stages/consent/models.py:52 msgid "Consent Stages" msgstr "" -#: authentik/stages/consent/models.py:67 +#: authentik/stages/consent/models.py:74 msgid "User Consent" msgstr "" -#: authentik/stages/consent/models.py:68 +#: authentik/stages/consent/models.py:75 msgid "User Consents" msgstr "" @@ -1645,27 +1653,27 @@ msgstr "" msgid "Log in" msgstr "" -#: authentik/stages/invitation/models.py:46 +#: authentik/stages/invitation/models.py:47 msgid "Invitation Stage" msgstr "" -#: authentik/stages/invitation/models.py:47 +#: authentik/stages/invitation/models.py:48 msgid "Invitation Stages" msgstr "" -#: authentik/stages/invitation/models.py:59 +#: authentik/stages/invitation/models.py:60 msgid "When enabled, the invitation will be deleted after usage." msgstr "" -#: authentik/stages/invitation/models.py:66 +#: authentik/stages/invitation/models.py:67 msgid "Optional fixed data to enforce on user enrollment." msgstr "" -#: authentik/stages/invitation/models.py:74 +#: authentik/stages/invitation/models.py:81 msgid "Invitation" msgstr "" -#: authentik/stages/invitation/models.py:75 +#: authentik/stages/invitation/models.py:82 msgid "Invitations" msgstr "" @@ -1817,16 +1825,16 @@ msgstr "" msgid "No Pending data." msgstr "" -#: authentik/tenants/models.py:18 +#: authentik/tenants/models.py:20 msgid "" "Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` " "and `ba.b`" msgstr "" -#: authentik/tenants/models.py:80 +#: authentik/tenants/models.py:87 msgid "Tenant" msgstr "" -#: authentik/tenants/models.py:81 +#: authentik/tenants/models.py:88 msgid "Tenants" msgstr "" diff --git a/schema.yml b/schema.yml index 52fb908a5..4b116de0c 100644 --- a/schema.yml +++ b/schema.yml @@ -5869,7 +5869,7 @@ paths: /flows/instances/{slug}/export/: get: operationId: flows_instances_export_retrieve - description: Export flow to .akflow file + description: Export flow to .yaml file parameters: - in: path name: slug @@ -6013,7 +6013,7 @@ paths: /flows/instances/import_flow/: post: operationId: flows_instances_import_flow_create - description: Import flow from .akflow file + description: Import flow from .yaml file tags: - flows requestBody: @@ -6030,6 +6030,215 @@ paths: description: Bad request '403': $ref: '#/components/schemas/GenericError' + /managed/blueprints/: + get: + operationId: managed_blueprints_list + description: Blueprint instances + parameters: + - in: query + name: name + schema: + type: string + - name: ordering + required: false + in: query + description: Which field to use when ordering the results. + schema: + type: string + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page_size + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - in: query + name: path + schema: + type: string + - name: search + required: false + in: query + description: A search term. + schema: + type: string + tags: + - managed + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedBlueprintInstanceList' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + post: + operationId: managed_blueprints_create + description: Blueprint instances + tags: + - managed + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstanceRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstance' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + /managed/blueprints/{instance_uuid}/: + get: + operationId: managed_blueprints_retrieve + description: Blueprint instances + parameters: + - in: path + name: instance_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Blueprint Instance. + required: true + tags: + - managed + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstance' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + put: + operationId: managed_blueprints_update + description: Blueprint instances + parameters: + - in: path + name: instance_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Blueprint Instance. + required: true + tags: + - managed + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstanceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstance' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + patch: + operationId: managed_blueprints_partial_update + description: Blueprint instances + parameters: + - in: path + name: instance_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Blueprint Instance. + required: true + tags: + - managed + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBlueprintInstanceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstance' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + delete: + operationId: managed_blueprints_destroy + description: Blueprint instances + parameters: + - in: path + name: instance_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Blueprint Instance. + required: true + tags: + - managed + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + /managed/blueprints/available/: + get: + operationId: managed_blueprints_available_list + description: Get blueprints + tags: + - managed + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + type: string + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' /oauth2/authorization_codes/: get: operationId: oauth2_authorization_codes_list @@ -8145,12 +8354,12 @@ paths: enum: - authentik.admin - authentik.api + - authentik.blueprints - authentik.core - authentik.crypto - authentik.events - authentik.flows - authentik.lib - - authentik.managed - authentik.outposts - authentik.policies - authentik.policies.dummy @@ -19607,7 +19816,7 @@ components: - authentik.stages.user_logout - authentik.stages.user_write - authentik.tenants - - authentik.managed + - authentik.blueprints - authentik.core type: string AppleChallengeResponseRequest: @@ -20653,6 +20862,60 @@ components: - POST - POST_AUTO type: string + BlueprintInstance: + type: object + description: Info about a single blueprint instance file + properties: + name: + type: string + path: + type: string + context: + type: object + additionalProperties: {} + last_applied: + type: string + format: date-time + readOnly: true + status: + $ref: '#/components/schemas/BlueprintInstanceStatusEnum' + enabled: + type: boolean + required: + - context + - last_applied + - name + - path + - status + BlueprintInstanceRequest: + type: object + description: Info about a single blueprint instance file + properties: + name: + type: string + minLength: 1 + path: + type: string + minLength: 1 + context: + type: object + additionalProperties: {} + status: + $ref: '#/components/schemas/BlueprintInstanceStatusEnum' + enabled: + type: boolean + required: + - context + - name + - path + - status + BlueprintInstanceStatusEnum: + enum: + - successful + - warning + - error + - unknown + type: string Cache: type: object description: Generic cache stats for an object @@ -24565,6 +24828,41 @@ components: required: - pagination - results + PaginatedBlueprintInstanceList: + type: object + properties: + pagination: + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + results: + type: array + items: + $ref: '#/components/schemas/BlueprintInstance' + required: + - pagination + - results PaginatedCaptchaStageList: type: object properties: @@ -27472,6 +27770,23 @@ components: minLength: 1 description: If any of the user's device has been used within this threshold, this stage will be skipped + PatchedBlueprintInstanceRequest: + type: object + description: Info about a single blueprint instance file + properties: + name: + type: string + minLength: 1 + path: + type: string + minLength: 1 + context: + type: object + additionalProperties: {} + status: + $ref: '#/components/schemas/BlueprintInstanceStatusEnum' + enabled: + type: boolean PatchedCaptchaStageRequest: type: object description: CaptchaStage Serializer @@ -31291,13 +31606,6 @@ components: maxLength: 16 required: - token - StatusEnum: - enum: - - SUCCESSFUL - - WARNING - - ERROR - - UNKNOWN - type: string SubModeEnum: enum: - hashed_user_id @@ -31406,7 +31714,7 @@ components: type: string format: date-time status: - $ref: '#/components/schemas/StatusEnum' + $ref: '#/components/schemas/TaskStatusEnum' messages: type: array items: {} @@ -31416,6 +31724,13 @@ components: - task_description - task_finish_timestamp - task_name + TaskStatusEnum: + enum: + - SUCCESSFUL + - WARNING + - ERROR + - UNKNOWN + type: string Tenant: type: object description: Tenant Serializer diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 1eb5bf33d..f90838f4f 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -25,10 +25,10 @@ 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.core.api.users import UserSerializer from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user -from authentik.managed.manager import ObjectManager RETRIES = int(environ.get("RETRIES", "3")) diff --git a/web/src/locales/de.po b/web/src/locales/de.po index ab149d36e..aa3c3317a 100644 --- a/web/src/locales/de.po +++ b/web/src/locales/de.po @@ -72,8 +72,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow Dateien, die auf goauthentik.io zu finden sind und von authentik exportiert werden können." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml Dateien, die auf goauthentik.io zu finden sind und von authentik exportiert werden können." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/en.po b/web/src/locales/en.po index c1b917a2c..392bd26bd 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -56,8 +56,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow files, which can be found on goauthentik.io and can be exported by authentik." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml files, which can be found on goauthentik.io and can be exported by authentik." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/es.po b/web/src/locales/es.po index 694a81e90..c03b2207b 100644 --- a/web/src/locales/es.po +++ b/web/src/locales/es.po @@ -59,8 +59,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow, que se pueden encontrar en goauthentik.io y que authentik puede exportar." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml, que se pueden encontrar en goauthentik.io y que authentik puede exportar." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index 0d435f4c8..b47474f65 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -62,7 +62,7 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." msgstr "" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts diff --git a/web/src/locales/pl.po b/web/src/locales/pl.po index e505b037a..1b2103225 100644 --- a/web/src/locales/pl.po +++ b/web/src/locales/pl.po @@ -59,8 +59,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr "Pliki .akflow, które można znaleźć na goauthentik.io i mogą być wyeksportowane przez authentik." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr "Pliki .yaml, które można znaleźć na goauthentik.io i mogą być wyeksportowane przez authentik." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/pl_PL.po b/web/src/locales/pl_PL.po index ffaf32933..d37a3bc92 100644 --- a/web/src/locales/pl_PL.po +++ b/web/src/locales/pl_PL.po @@ -51,10 +51,10 @@ msgstr "-" #: src/pages/flows/FlowImportForm.ts msgid "" -".akflow files, which can be found on goauthentik.io and can be exported by " +".yaml files, which can be found on goauthentik.io and can be exported by " "authentik." msgstr "" -"Pliki .akflow, które można znaleźć na goauthentik.io i mogą być " +"Pliki .yaml, które można znaleźć na goauthentik.io i mogą być " "wyeksportowane przez authentik." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 7a2c8370e..16e6d5719 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -56,7 +56,7 @@ msgid "-" msgstr "" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." msgstr "" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 00f8de0d2..682725b70 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -59,8 +59,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow dosyaları, goauthentik.io'da bulunabilir ve authentik tarafından ihraç edilebilir." +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml dosyaları, goauthentik.io'da bulunabilir ve authentik tarafından ihraç edilebilir." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/zh-Hans.po b/web/src/locales/zh-Hans.po index 11e3975e0..fef7ccb21 100644 --- a/web/src/locales/zh-Hans.po +++ b/web/src/locales/zh-Hans.po @@ -60,8 +60,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow 文件,可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml 文件,可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/zh-Hant.po b/web/src/locales/zh-Hant.po index 1ad167005..2db5fc21b 100644 --- a/web/src/locales/zh-Hant.po +++ b/web/src/locales/zh-Hant.po @@ -61,8 +61,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow 文件,这些文件可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml 文件,这些文件可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/zh_CN.po b/web/src/locales/zh_CN.po index 6d19c8c2d..8df1c7066 100644 --- a/web/src/locales/zh_CN.po +++ b/web/src/locales/zh_CN.po @@ -56,9 +56,9 @@ msgstr "-" #: src/pages/flows/FlowImportForm.ts msgid "" -".akflow files, which can be found on goauthentik.io and can be exported by " +".yaml files, which can be found on goauthentik.io and can be exported by " "authentik." -msgstr ".akflow 文件,可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" +msgstr ".yaml 文件,可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/locales/zh_TW.po b/web/src/locales/zh_TW.po index c209a9138..cd4aba895 100644 --- a/web/src/locales/zh_TW.po +++ b/web/src/locales/zh_TW.po @@ -61,8 +61,8 @@ msgid "-" msgstr "-" #: src/pages/flows/FlowImportForm.ts -msgid ".akflow files, which can be found on goauthentik.io and can be exported by authentik." -msgstr ".akflow 文件,这些文件可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" +msgid ".yaml files, which can be found on goauthentik.io and can be exported by authentik." +msgstr ".yaml 文件,这些文件可以在 goauthentik.io 上找到,也可以通过 authentik 导出。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "6 digits, widely compatible" diff --git a/web/src/pages/admin-overview/charts/LDAPSyncStatusChart.ts b/web/src/pages/admin-overview/charts/LDAPSyncStatusChart.ts index 5749d5b9d..c9149ce26 100644 --- a/web/src/pages/admin-overview/charts/LDAPSyncStatusChart.ts +++ b/web/src/pages/admin-overview/charts/LDAPSyncStatusChart.ts @@ -7,7 +7,7 @@ import { t } from "@lingui/macro"; import { customElement } from "lit/decorators.js"; -import { SourcesApi, StatusEnum } from "@goauthentik/api"; +import { SourcesApi, TaskStatusEnum } from "@goauthentik/api"; interface LDAPSyncStats { healthy: number; @@ -50,7 +50,7 @@ export class LDAPSyncStatusChart extends AKChart { }); health.forEach((task) => { - if (task.status !== StatusEnum.Successful) { + if (task.status !== TaskStatusEnum.Successful) { sourceKey = "failed"; } const now = new Date().getTime(); diff --git a/web/src/pages/flows/FlowImportForm.ts b/web/src/pages/flows/FlowImportForm.ts index d4fc551b5..16feae700 100644 --- a/web/src/pages/flows/FlowImportForm.ts +++ b/web/src/pages/flows/FlowImportForm.ts @@ -32,7 +32,7 @@ export class FlowImportForm extends Form {

- ${t`.akflow files, which can be found on goauthentik.io and can be exported by authentik.`} + ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}

`; diff --git a/web/src/pages/sources/ldap/LDAPSourceViewPage.ts b/web/src/pages/sources/ldap/LDAPSourceViewPage.ts index 83a7acfd1..a4dfe3e3a 100644 --- a/web/src/pages/sources/ldap/LDAPSourceViewPage.ts +++ b/web/src/pages/sources/ldap/LDAPSourceViewPage.ts @@ -24,7 +24,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { LDAPSource, SourcesApi, StatusEnum } from "@goauthentik/api"; +import { LDAPSource, SourcesApi, TaskStatusEnum } from "@goauthentik/api"; @customElement("ak-source-ldap-view") export class LDAPSourceViewPage extends LitElement { @@ -145,9 +145,9 @@ export class LDAPSourceViewPage extends LitElement { return html`
    ${tasks.map((task) => { let header = ""; - if (task.status === StatusEnum.Warning) { + if (task.status === TaskStatusEnum.Warning) { header = t`Task finished with warnings`; - } else if (task.status === StatusEnum.Error) { + } else if (task.status === TaskStatusEnum.Error) { header = t`Task finished with errors`; } else { header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`; diff --git a/web/src/pages/system-tasks/SystemTaskListPage.ts b/web/src/pages/system-tasks/SystemTaskListPage.ts index f074feb4a..1a3dae35c 100644 --- a/web/src/pages/system-tasks/SystemTaskListPage.ts +++ b/web/src/pages/system-tasks/SystemTaskListPage.ts @@ -14,7 +14,7 @@ import { customElement, property } from "lit/decorators.js"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; -import { AdminApi, StatusEnum, Task } from "@goauthentik/api"; +import { AdminApi, Task, TaskStatusEnum } from "@goauthentik/api"; @customElement("ak-system-task-list") export class SystemTaskListPage extends TablePage { @@ -67,11 +67,11 @@ export class SystemTaskListPage extends TablePage { taskStatus(task: Task): TemplateResult { switch (task.status) { - case StatusEnum.Successful: + case TaskStatusEnum.Successful: return html`${t`Successful`}`; - case StatusEnum.Warning: + case TaskStatusEnum.Warning: return html`${t`Warning`}`; - case StatusEnum.Error: + case TaskStatusEnum.Error: return html`${t`Error`}`; default: return html`${t`Unknown`}`; diff --git a/website/docs/flow/examples/flows.md b/website/docs/flow/examples/flows.md index dbb40e137..2fccb023f 100644 --- a/website/docs/flow/examples/flows.md +++ b/website/docs/flow/examples/flows.md @@ -12,13 +12,13 @@ The example flows provided below will **override** the default flows, please rev ## Enrollment (2 Stage) -Flow: right-click [here](/flows/enrollment-2-stage.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-enrollment-2-stage.yaml) and save the file. Sign-up flow for new users, which prompts them for their username, email, password and name. No verification is done. Users are also immediately logged on after this flow. ## Enrollment with email verification -Flow: right-click [here](/flows/enrollment-email-verification.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-enrollment-email-verification.yaml) and save the file. Same flow as above, with an extra email verification stage. @@ -26,13 +26,13 @@ You'll probably have to adjust the Email stage and set your connection details. ## Two-factor Login -Flow: right-click [here](/flows/login-2fa.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-login-2fa.yaml) and save the file. Login flow which follows the default pattern (username/email, then password), but also checks for the user's OTP token, if they have one configured ## Login with conditional Captcha -Flow: right-click [here](/flows/login-conditional-captcha.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-login-conditional-captcha.yaml) and save the file. Login flow which conditionally shows the users a captcha, based on the reputation of their IP and Username. @@ -40,13 +40,13 @@ By default, the captcha test keys are used. You can get a proper key [here](http ## Recovery with email verification -Flow: right-click [here](/flows/recovery-email-verification.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-recovery-email-verification.yaml) and save the file. Recovery flow, the user is sent an email after they've identified themselves. After they click on the link in the email, they are prompted for a new password and immediately logged on. ## User deletion -Flow: right-click [here](/flows/unenrollment.akflow) and save the file. +Flow: right-click [here](/blueprints/example/flows-unenrollment.yaml) and save the file. Flow for users to delete their account, diff --git a/website/static/blueprints b/website/static/blueprints new file mode 120000 index 000000000..92f24caf8 --- /dev/null +++ b/website/static/blueprints @@ -0,0 +1 @@ +../../blueprints/ \ No newline at end of file