diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index 3785b9d1a..d81f08ecc 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -24,7 +24,7 @@ jobs:
echo "AUTHENTIK_TAG=latest" >> .env
docker-compose up --no-start
docker-compose start postgresql redis
- docker-compose run -u root server test
+ docker-compose run -u root server test-all
- name: Extract version number
id: get_version
uses: actions/github-script@v6
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 25b17a816..eac30921c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -27,5 +27,8 @@
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib",
- "typescript.enablePromptUseWorkspaceTsdk": true
+ "typescript.enablePromptUseWorkspaceTsdk": true,
+ "yaml.schemas": {
+ "./blueprints/schema.json": "blueprints/**/*.yaml"
+ }
}
diff --git a/Makefile b/Makefile
index fbeadf840..9656db580 100644
--- a/Makefile
+++ b/Makefile
@@ -52,10 +52,11 @@ lint:
i18n-extract: i18n-extract-core web-extract
i18n-extract-core:
- ./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
+ ak makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
gen-build:
- AUTHENTIK_DEBUG=true ./manage.py spectacular --file schema.yml
+ AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
+ AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
gen-clean:
rm -rf web/api/src/
@@ -168,7 +169,7 @@ ci-pyright: ci--meta-debug
pyright e2e lifecycle
ci-pending-migrations: ci--meta-debug
- ./manage.py makemigrations --check
+ ak makemigrations --check
install: web-install website-install
poetry install
diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py
index 0a3345d13..97e610114 100644
--- a/authentik/blueprints/api.py
+++ b/authentik/blueprints/api.py
@@ -15,6 +15,7 @@ from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.tasks import BlueprintFile, apply_blueprint, blueprints_find
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
+from authentik.events.utils import sanitize_dict
class ManagedSerializer:
@@ -85,7 +86,7 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
def available(self, request: Request) -> Response:
"""Get blueprints"""
files: list[BlueprintFile] = blueprints_find.delay().get()
- return Response([asdict(file) for file in files])
+ return Response([sanitize_dict(asdict(file)) for file in files])
@permission_required("authentik_blueprints.view_blueprintinstance")
@extend_schema(
diff --git a/authentik/blueprints/management/commands/make_blueprint_schema.py b/authentik/blueprints/management/commands/make_blueprint_schema.py
new file mode 100644
index 000000000..d5c65b762
--- /dev/null
+++ b/authentik/blueprints/management/commands/make_blueprint_schema.py
@@ -0,0 +1,35 @@
+"""Generate JSON Schema for blueprints"""
+from json import dumps, loads
+from pathlib import Path
+
+from django.apps import apps
+from django.core.management.base import BaseCommand, no_translations
+from structlog.stdlib import get_logger
+
+from authentik.blueprints.v1.importer import is_model_allowed
+
+LOGGER = get_logger()
+
+
+class Command(BaseCommand):
+ """Generate JSON Schema for blueprints"""
+
+ schema: dict
+
+ @no_translations
+ def handle(self, *args, **options):
+ """Generate JSON Schema for blueprints"""
+ path = Path(__file__).parent.joinpath("./schema_template.json")
+ with open(path, "r", encoding="utf-8") as _template_file:
+ self.schema = loads(_template_file.read())
+ self.set_model_allowed()
+ self.stdout.write(dumps(self.schema, indent=4))
+
+ def set_model_allowed(self):
+ """Set model enum"""
+ model_names = []
+ for model in apps.get_models():
+ if not is_model_allowed(model):
+ continue
+ model_names.append(f"{model._meta.app_label}.{model._meta.model_name}")
+ self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names
diff --git a/authentik/blueprints/management/commands/schema_template.json b/authentik/blueprints/management/commands/schema_template.json
new file mode 100644
index 000000000..cf1bf2b00
--- /dev/null
+++ b/authentik/blueprints/management/commands/schema_template.json
@@ -0,0 +1,84 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/example.json",
+ "type": "object",
+ "title": "authentik Blueprint schema",
+ "default": {},
+ "required": [
+ "version",
+ "entries"
+ ],
+ "properties": {
+ "version": {
+ "$id": "#/properties/version",
+ "type": "integer",
+ "title": "Blueprint version",
+ "default": 1
+ },
+ "metadata": {
+ "$id": "#/properties/metadata",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "labels": {
+ "type": "object"
+ }
+ }
+ },
+ "entries": {
+ "type": "array",
+ "items": {
+ "$id": "#entry",
+ "type": "object",
+ "required": [
+ "model",
+ "identifiers"
+ ],
+ "properties": {
+ "model": {
+ "type": "string",
+ "enum": [
+ "placeholder"
+ ]
+ },
+ "id": {
+ "type": "string"
+ },
+ "attrs": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Commonly available field, may not exist on all models"
+ }
+ },
+ "additionalProperties": true
+ },
+ "identifiers": {
+ "type": "object",
+ "properties": {
+ "pk": {
+ "description": "Commonly available field, may not exist on all models",
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "format": "uuid"
+ }
+ ]
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/authentik/blueprints/migrations/0001_initial.py b/authentik/blueprints/migrations/0001_initial.py
index 7da7395f6..6496a023c 100644
--- a/authentik/blueprints/migrations/0001_initial.py
+++ b/authentik/blueprints/migrations/0001_initial.py
@@ -11,6 +11,7 @@ from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from yaml import load
+from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_SYSTEM
from authentik.lib.config import CONFIG
@@ -37,7 +38,7 @@ def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path:
if not instance:
instance = BlueprintInstance(
name=meta.name if meta else str(rel_path),
- path=str(path),
+ path=str(rel_path),
context={},
status=BlueprintInstanceStatus.UNKNOWN,
enabled=True,
@@ -62,7 +63,7 @@ def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
if Flow.objects.using(db_alias).all().exists():
blueprint.enabled = False
# System blueprints are always enabled
- if "/system/" in blueprint.path:
+ if blueprint.metadata.get("labels", {}).get(LABEL_AUTHENTIK_SYSTEM, "").lower() == "true":
blueprint.enabled = True
blueprint.save()
diff --git a/authentik/blueprints/tests/test_models.py b/authentik/blueprints/tests/test_models.py
index 0dc9ef5a1..a27aa5493 100644
--- a/authentik/blueprints/tests/test_models.py
+++ b/authentik/blueprints/tests/test_models.py
@@ -4,7 +4,7 @@ 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.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
@@ -29,6 +29,6 @@ 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:
+ if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
diff --git a/authentik/blueprints/tests/test_v1_api.py b/authentik/blueprints/tests/test_v1_api.py
new file mode 100644
index 000000000..35dc8f8ff
--- /dev/null
+++ b/authentik/blueprints/tests/test_v1_api.py
@@ -0,0 +1,45 @@
+"""Test blueprints v1 api"""
+from json import loads
+from tempfile import NamedTemporaryFile, mkdtemp
+
+from django.urls import reverse
+from rest_framework.test import APITestCase
+from yaml import dump
+
+from authentik.core.tests.utils import create_test_admin_user
+from authentik.lib.config import CONFIG
+
+TMP = mkdtemp("authentik-blueprints")
+
+
+class TestBlueprintsV1API(APITestCase):
+ """Test Blueprints API"""
+
+ def setUp(self) -> None:
+ self.user = create_test_admin_user()
+ self.client.force_login(self.user)
+
+ @CONFIG.patch("blueprints_dir", TMP)
+ def test_api_available(self):
+ """Test valid file"""
+ with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
+ file.write(
+ dump(
+ {
+ "version": 1,
+ "entries": [],
+ }
+ )
+ )
+ file.flush()
+ res = self.client.get(reverse("authentik_api:blueprintinstance-available"))
+ self.assertEqual(res.status_code, 200)
+ response = loads(res.content.decode())
+ self.assertEqual(len(response), 1)
+ self.assertEqual(
+ response[0]["hash"],
+ (
+ "e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1bd1f9b352"
+ "6871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
+ ),
+ )
diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py
index 5a7ac8a1c..87fc8aeda 100644
--- a/authentik/blueprints/v1/importer.py
+++ b/authentik/blueprints/v1/importer.py
@@ -35,19 +35,29 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
-EXCLUDED_MODELS = (
- # Base classes
- Provider,
- Source,
- PropertyMapping,
- UserSourceConnection,
- Stage,
- OutpostServiceConnection,
- Policy,
- PolicyBindingModel,
- # Classes that have other dependencies
- AuthenticatedSession,
-)
+
+def is_model_allowed(model: type[Model]) -> bool:
+ """Check if model is allowed"""
+ # pylint: disable=imported-auth-user
+ from django.contrib.auth.models import Group as DjangoGroup
+ from django.contrib.auth.models import User as DjangoUser
+
+ excluded_models = (
+ DjangoUser,
+ DjangoGroup,
+ # Base classes
+ Provider,
+ Source,
+ PropertyMapping,
+ UserSourceConnection,
+ Stage,
+ OutpostServiceConnection,
+ Policy,
+ PolicyBindingModel,
+ # Classes that have other dependencies
+ AuthenticatedSession,
+ )
+ return model not in excluded_models
@contextmanager
@@ -123,8 +133,10 @@ class Importer:
model_app_label, model_name = entry.model.split(".")
model: type[SerializerModel] = apps.get_model(model_app_label, model_name)
# Don't use isinstance since we don't want to check for inheritance
- if model in EXCLUDED_MODELS:
+ if not is_model_allowed(model):
raise EntryInvalidError(f"Model {model} not allowed")
+ if entry.identifiers == {}:
+ raise EntryInvalidError("No identifiers")
# If we try to validate without referencing a possible instance
# we'll get a duplicate error, hence we load the model here and return
@@ -148,6 +160,7 @@ class Importer:
pk=model_instance.pk,
)
serializer_kwargs["instance"] = model_instance
+ serializer_kwargs["partial"] = True
else:
self.logger.debug("initialise new instance", model=model, **updated_identifiers)
model_instance = model()
diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py
index 16ca31f2d..5325da58d 100644
--- a/authentik/blueprints/v1/tasks.py
+++ b/authentik/blueprints/v1/tasks.py
@@ -1,6 +1,5 @@
"""v1 blueprints tasks"""
from dataclasses import asdict, dataclass, field
-from glob import glob
from hashlib import sha512
from pathlib import Path
from typing import Optional
@@ -43,7 +42,8 @@ class BlueprintFile:
def blueprints_find():
"""Find blueprints and return valid ones"""
blueprints = []
- for file in glob(f"{CONFIG.y('blueprints_dir')}/**/*.yaml", recursive=True):
+ root = Path(CONFIG.y("blueprints_dir"))
+ for file in root.glob("**/*.yaml"):
path = Path(file)
with open(path, "r", encoding="utf-8") as blueprint_file:
try:
@@ -57,7 +57,7 @@ def blueprints_find():
if version != 1:
continue
file_hash = sha512(path.read_bytes()).hexdigest()
- blueprint = BlueprintFile(str(path), version, file_hash, path.stat().st_mtime)
+ blueprint = BlueprintFile(path.relative_to(root), version, file_hash, path.stat().st_mtime)
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
if (
blueprint.meta
@@ -88,11 +88,10 @@ def blueprints_discover(self: MonitoredTask):
def check_blueprint_v1_file(blueprint: BlueprintFile):
"""Check if blueprint should be imported"""
- rel_path = Path(blueprint.path).relative_to(Path(CONFIG.y("blueprints_dir")))
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=blueprint.path).first()
if not instance:
instance = BlueprintInstance(
- name=blueprint.meta.name if blueprint.meta else str(rel_path),
+ name=blueprint.meta.name if blueprint.meta else str(blueprint.path),
path=blueprint.path,
context={},
status=BlueprintInstanceStatus.UNKNOWN,
@@ -119,8 +118,9 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
if not instance or not instance.enabled:
return
- file_hash = sha512(Path(instance.path).read_bytes()).hexdigest()
- with open(instance.path, "r", encoding="utf-8") as blueprint_file:
+ full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(instance.path))
+ file_hash = sha512(full_path.read_bytes()).hexdigest()
+ with open(full_path, "r", encoding="utf-8") as blueprint_file:
importer = Importer(blueprint_file.read())
valid, logs = importer.validate()
if not valid:
diff --git a/authentik/events/migrations/0002_alter_notificationtransport_mode.py b/authentik/events/migrations/0002_alter_notificationtransport_mode.py
index 44c60ae76..5807b42e4 100644
--- a/authentik/events/migrations/0002_alter_notificationtransport_mode.py
+++ b/authentik/events/migrations/0002_alter_notificationtransport_mode.py
@@ -1,29 +1,5 @@
# Generated by Django 4.0.4 on 2022-05-30 18:08
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.events.models import TransportMode
-
-
-def notify_local_transport(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- db_alias = schema_editor.connection.alias
- NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
- NotificationRule = apps.get_model("authentik_events", "NotificationRule")
-
- local_transport, _ = NotificationTransport.objects.using(db_alias).update_or_create(
- name="default-local-transport",
- defaults={"mode": TransportMode.LOCAL},
- )
-
- for trigger in NotificationRule.objects.using(db_alias).filter(
- name__in=[
- "default-notify-configuration-error",
- "default-notify-exception",
- "default-notify-update",
- ]
- ):
- trigger.transports.add(local_transport)
class Migration(migrations.Migration):
@@ -46,5 +22,4 @@ class Migration(migrations.Migration):
default="local",
),
),
- migrations.RunPython(notify_local_transport),
]
diff --git a/authentik/events/migrations/0011_notification_rules_default_v1.py b/authentik/events/migrations/0011_notification_rules_default_v1.py
index efa9ea64c..a011d9380 100644
--- a/authentik/events/migrations/0011_notification_rules_default_v1.py
+++ b/authentik/events/migrations/0011_notification_rules_default_v1.py
@@ -1,130 +1,6 @@
# Generated by Django 3.1.4 on 2021-01-10 18:57
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.events.models import EventAction, NotificationSeverity, TransportMode
-
-
-def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- db_alias = schema_editor.connection.alias
- Group = apps.get_model("authentik_core", "Group")
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
- EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
- NotificationRule = apps.get_model("authentik_events", "NotificationRule")
- NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
-
- admin_group = (
- Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
- )
-
- policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
- name="default-match-configuration-error",
- defaults={"action": EventAction.CONFIGURATION_ERROR},
- )
- trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
- name="default-notify-configuration-error",
- defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
- )
- trigger.transports.set(
- NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
- )
- trigger.save()
- PolicyBinding.objects.using(db_alias).update_or_create(
- target=trigger,
- policy=policy,
- defaults={
- "order": 0,
- },
- )
-
-
-def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- db_alias = schema_editor.connection.alias
- Group = apps.get_model("authentik_core", "Group")
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
- EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
- NotificationRule = apps.get_model("authentik_events", "NotificationRule")
- NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
-
- admin_group = (
- Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
- )
-
- policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
- name="default-match-update",
- defaults={"action": EventAction.UPDATE_AVAILABLE},
- )
- trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
- name="default-notify-update",
- defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
- )
- trigger.transports.set(
- NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
- )
- trigger.save()
- PolicyBinding.objects.using(db_alias).update_or_create(
- target=trigger,
- policy=policy,
- defaults={
- "order": 0,
- },
- )
-
-
-def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- db_alias = schema_editor.connection.alias
- Group = apps.get_model("authentik_core", "Group")
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
- EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
- NotificationRule = apps.get_model("authentik_events", "NotificationRule")
- NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
-
- admin_group = (
- Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
- )
-
- policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
- name="default-match-policy-exception",
- defaults={"action": EventAction.POLICY_EXCEPTION},
- )
- policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
- name="default-match-property-mapping-exception",
- defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
- )
- trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
- name="default-notify-exception",
- defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
- )
- trigger.transports.set(
- NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
- )
- trigger.save()
- PolicyBinding.objects.using(db_alias).update_or_create(
- target=trigger,
- policy=policy_policy_exc,
- defaults={
- "order": 0,
- },
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- target=trigger,
- policy=policy_pm_exc,
- defaults={
- "order": 1,
- },
- )
-
-
-def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- db_alias = schema_editor.connection.alias
- NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
-
- NotificationTransport.objects.using(db_alias).update_or_create(
- name="default-email-transport",
- defaults={"mode": TransportMode.EMAIL},
- )
class Migration(migrations.Migration):
@@ -134,14 +10,6 @@ class Migration(migrations.Migration):
"authentik_events",
"0010_notification_notificationtransport_notificationrule",
),
- ("authentik_core", "0016_auto_20201202_2234"),
- ("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
- ("authentik_policies", "0004_policy_execution_logging"),
]
- operations = [
- migrations.RunPython(transport_email_global),
- migrations.RunPython(notify_configuration_error),
- migrations.RunPython(notify_update),
- migrations.RunPython(notify_exception),
- ]
+ operations = []
diff --git a/authentik/events/utils.py b/authentik/events/utils.py
index f23c6d533..325714ea7 100644
--- a/authentik/events/utils.py
+++ b/authentik/events/utils.py
@@ -1,6 +1,7 @@
"""event utilities"""
import re
from dataclasses import asdict, is_dataclass
+from pathlib import Path
from typing import Any, Optional
from uuid import UUID
@@ -97,6 +98,8 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
continue
elif isinstance(value, City):
final_dict[key] = GEOIP_READER.city_to_dict(value)
+ elif isinstance(value, Path):
+ final_dict[key] = str(value)
elif isinstance(value, type):
final_dict[key] = {
"type": value.__name__,
diff --git a/authentik/flows/migrations/0008_default_flows.py b/authentik/flows/migrations/0008_default_flows.py
index d507f209c..9cf6afd78 100644
--- a/authentik/flows/migrations/0008_default_flows.py
+++ b/authentik/flows/migrations/0008_default_flows.py
@@ -1,103 +1,12 @@
# Generated by Django 3.0.3 on 2020-05-08 14:30
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-from authentik.stages.identification.models import UserFields
-from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
-
-
-def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- PasswordStage = apps.get_model("authentik_stages_password", "PasswordStage")
- UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
- IdentificationStage = apps.get_model("authentik_stages_identification", "IdentificationStage")
- db_alias = schema_editor.connection.alias
-
- identification_stage, _ = IdentificationStage.objects.using(db_alias).update_or_create(
- name="default-authentication-identification",
- defaults={
- "user_fields": [UserFields.E_MAIL, UserFields.USERNAME],
- },
- )
-
- password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
- name="default-authentication-password",
- defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
- )
-
- login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
- name="default-authentication-login"
- )
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-authentication-flow",
- designation=FlowDesignation.AUTHENTICATION,
- defaults={
- "name": "Welcome to authentik!",
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=identification_stage,
- defaults={
- "order": 10,
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=password_stage,
- defaults={
- "order": 20,
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=login_stage,
- defaults={
- "order": 100,
- },
- )
-
-
-def create_default_invalidation_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- UserLogoutStage = apps.get_model("authentik_stages_user_logout", "UserLogoutStage")
- db_alias = schema_editor.connection.alias
-
- UserLogoutStage.objects.using(db_alias).update_or_create(name="default-invalidation-logout")
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-invalidation-flow",
- designation=FlowDesignation.INVALIDATION,
- defaults={
- "name": "Logout",
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=UserLogoutStage.objects.using(db_alias).first(),
- defaults={
- "order": 0,
- },
- )
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0007_auto_20200703_2059"),
- ("authentik_stages_user_login", "0001_initial"),
- ("authentik_stages_user_logout", "0001_initial"),
- ("authentik_stages_password", "0001_initial"),
- ("authentik_stages_identification", "0001_initial"),
]
- operations = [
- migrations.RunPython(create_default_authentication_flow),
- migrations.RunPython(create_default_invalidation_flow),
- ]
+ operations = []
diff --git a/authentik/flows/migrations/0009_source_flows.py b/authentik/flows/migrations/0009_source_flows.py
index 1662311f3..747181e76 100644
--- a/authentik/flows/migrations/0009_source_flows.py
+++ b/authentik/flows/migrations/0009_source_flows.py
@@ -1,151 +1,12 @@
# Generated by Django 3.0.6 on 2020-05-23 15:47
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-from authentik.stages.prompt.models import FieldTypes
-
-FLOW_POLICY_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"""
-PROMPT_POLICY_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', {})"""
-
-
-def create_default_source_enrollment_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
-
- ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
-
- PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
- Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
- UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
- UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
-
- db_alias = schema_editor.connection.alias
-
- # Create a policy that only allows this flow when doing an SSO Request
- flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-source-enrollment-if-sso",
- defaults={"expression": FLOW_POLICY_EXPRESSION},
- )
-
- # This creates a Flow used by sources to enroll users
- # It makes sure that a username is set, and if not, prompts the user for a Username
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-source-enrollment",
- designation=FlowDesignation.ENROLLMENT,
- defaults={
- "name": "Welcome to authentik! Please select a username.",
- },
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- policy=flow_policy, target=flow, defaults={"order": 0}
- )
-
- # PromptStage to ask user for their username
- prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
- name="default-source-enrollment-prompt",
- )
- prompt, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="username",
- defaults={
- "label": "Username",
- "type": FieldTypes.TEXT,
- "required": True,
- "placeholder": "Username",
- "order": 100,
- },
- )
- prompt_stage.fields.add(prompt)
-
- # Policy to only trigger prompt when no username is given
- prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-source-enrollment-if-username",
- defaults={"expression": PROMPT_POLICY_EXPRESSION},
- )
-
- # UserWrite stage to create the user, and login stage to log user in
- user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
- name="default-source-enrollment-write"
- )
- user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
- name="default-source-enrollment-login"
- )
-
- binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=prompt_stage,
- defaults={"order": 0, "re_evaluate_policies": True},
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- policy=prompt_policy, target=binding, defaults={"order": 0}
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=user_write, defaults={"order": 1}
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=user_login, defaults={"order": 2}
- )
-
-
-def create_default_source_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
-
- ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
-
- UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
-
- db_alias = schema_editor.connection.alias
-
- # Create a policy that only allows this flow when doing an SSO Request
- flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-source-authentication-if-sso",
- defaults={
- "expression": FLOW_POLICY_EXPRESSION,
- },
- )
-
- # This creates a Flow used by sources to authenticate users
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-source-authentication",
- designation=FlowDesignation.AUTHENTICATION,
- defaults={
- "name": "Welcome to authentik!",
- },
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- policy=flow_policy, target=flow, defaults={"order": 0}
- )
-
- user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
- name="default-source-authentication-login"
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=user_login, defaults={"order": 0}
- )
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0008_default_flows"),
- ("authentik_policies", "0001_initial"),
- ("authentik_policies_expression", "0001_initial"),
- ("authentik_stages_prompt", "0001_initial"),
- ("authentik_stages_user_write", "0001_initial"),
- ("authentik_stages_user_login", "0001_initial"),
]
- operations = [
- migrations.RunPython(create_default_source_enrollment_flow),
- migrations.RunPython(create_default_source_authentication_flow),
- ]
+ operations = []
diff --git a/authentik/flows/migrations/0010_provider_flows.py b/authentik/flows/migrations/0010_provider_flows.py
index 45eafcb0d..483432366 100644
--- a/authentik/flows/migrations/0010_provider_flows.py
+++ b/authentik/flows/migrations/0010_provider_flows.py
@@ -1,46 +1,12 @@
# Generated by Django 3.0.6 on 2020-05-24 11:34
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-
-
-def create_default_provider_authorization_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- ConsentStage = apps.get_model("authentik_stages_consent", "ConsentStage")
-
- db_alias = schema_editor.connection.alias
-
- # Empty flow for providers where consent is implicitly given
- Flow.objects.using(db_alias).update_or_create(
- slug="default-provider-authorization-implicit-consent",
- designation=FlowDesignation.AUTHORIZATION,
- defaults={"name": "Authorize Application"},
- )
-
- # Flow with consent form to obtain explicit user consent
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-provider-authorization-explicit-consent",
- designation=FlowDesignation.AUTHORIZATION,
- defaults={"name": "Authorize Application"},
- )
- stage, _ = ConsentStage.objects.using(db_alias).update_or_create(
- name="default-provider-authorization-consent"
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=stage, defaults={"order": 0}
- )
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0009_source_flows"),
- ("authentik_stages_consent", "0001_initial"),
]
- operations = [migrations.RunPython(create_default_provider_authorization_flow)]
+ operations = []
diff --git a/authentik/flows/migrations/0011_flow_title.py b/authentik/flows/migrations/0011_flow_title.py
index 5f397dfc5..994b41902 100644
--- a/authentik/flows/migrations/0011_flow_title.py
+++ b/authentik/flows/migrations/0011_flow_title.py
@@ -1,27 +1,5 @@
# Generated by Django 3.1 on 2020-08-28 13:14
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-
-def add_title_for_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- slug_title_map = {
- "default-authentication-flow": "Welcome to authentik!",
- "default-invalidation-flow": "Default Invalidation Flow",
- "default-source-enrollment": "Welcome to authentik! Please select a username.",
- "default-source-authentication": "Welcome to authentik!",
- "default-provider-authorization-implicit-consent": "Redirecting to %(app)s",
- "default-provider-authorization-explicit-consent": "Redirecting to %(app)s",
- "default-password-change": "Change password",
- }
- db_alias = schema_editor.connection.alias
- Flow = apps.get_model("authentik_flows", "Flow")
- for flow in Flow.objects.using(db_alias).all():
- if flow.slug in slug_title_map:
- flow.title = slug_title_map[flow.slug]
- else:
- flow.title = flow.name
- flow.save()
class Migration(migrations.Migration):
@@ -45,7 +23,6 @@ class Migration(migrations.Migration):
field=models.TextField(default="", blank=True),
preserve_default=False,
),
- migrations.RunPython(add_title_for_defaults),
migrations.AlterField(
model_name="flow",
name="title",
diff --git a/authentik/flows/migrations/0014_auto_20200925_2332.py b/authentik/flows/migrations/0014_auto_20200925_2332.py
index 15f7f4d5f..267511832 100644
--- a/authentik/flows/migrations/0014_auto_20200925_2332.py
+++ b/authentik/flows/migrations/0014_auto_20200925_2332.py
@@ -1,27 +1,6 @@
# Generated by Django 3.1.1 on 2020-09-25 23:32
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-
-# First stage for default-source-enrollment flow (prompt stage)
-# needs to have its policy re-evaluated
-def update_default_source_enrollment_flow_binding(
- apps: Apps, schema_editor: BaseDatabaseSchemaEditor
-):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- db_alias = schema_editor.connection.alias
-
- flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
- if not flows.exists():
- return
- flow = flows.first()
-
- binding = FlowStageBinding.objects.get(target=flow, order=0)
- binding.re_evaluate_policies = True
- binding.save()
class Migration(migrations.Migration):
@@ -47,5 +26,4 @@ class Migration(migrations.Migration):
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
),
),
- migrations.RunPython(update_default_source_enrollment_flow_binding),
]
diff --git a/authentik/flows/migrations/0018_oob_flows.py b/authentik/flows/migrations/0018_oob_flows.py
index 875f85f5b..8f2e021a5 100644
--- a/authentik/flows/migrations/0018_oob_flows.py
+++ b/authentik/flows/migrations/0018_oob_flows.py
@@ -1,141 +1,12 @@
# Generated by Django 3.1.7 on 2021-04-06 13:25
-from django.apps.registry import Apps
-from django.contrib.auth.hashers import is_password_usable
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-
-PW_USABLE_POLICY_EXPRESSION = """# This policy ensures that the setup flow can only be
-# executed when the admin user doesn't have a password set
-akadmin = ak_user_by(username="akadmin")
-return not akadmin.has_usable_password()"""
-PREFILL_POLICY_EXPRESSION = """# This policy sets the user for the currently running flow
-# by injecting "pending_user"
-akadmin = ak_user_by(username="akadmin")
-context["flow_plan"].context["pending_user"] = akadmin
-return True"""
-
-
-def create_default_oobe_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- from authentik.stages.prompt.models import FieldTypes
-
- User = apps.get_model("authentik_core", "User")
-
- PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
- UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
- PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
- Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
-
- ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
-
- db_alias = schema_editor.connection.alias
-
- # Only create the flow if the akadmin user exists,
- # and has an un-usable password
- akadmins = User.objects.filter(username="akadmin")
- if not akadmins.exists():
- return
- akadmin = akadmins.first()
- if is_password_usable(akadmin.password):
- return
-
- # Create a policy that sets the flow's user
- prefill_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-oobe-prefill-user",
- defaults={"expression": PREFILL_POLICY_EXPRESSION},
- )
- password_usable_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-oobe-password-usable",
- defaults={"expression": PW_USABLE_POLICY_EXPRESSION},
- )
-
- prompt_header, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="oobe-header-text",
- defaults={
- "label": "oobe-header-text",
- "type": FieldTypes.STATIC,
- "placeholder": "Welcome to authentik! Please set a password for the default admin user, akadmin.",
- "order": 100,
- },
- )
- prompt_email, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="email",
- defaults={
- "label": "Email",
- "type": FieldTypes.EMAIL,
- "placeholder": "Admin email",
- "order": 101,
- },
- )
- password_first = Prompt.objects.using(db_alias).get(field_key="password")
- password_second = Prompt.objects.using(db_alias).get(field_key="password_repeat")
-
- prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
- name="default-oobe-password",
- )
- prompt_stage.fields.set([prompt_header, prompt_email, password_first, password_second])
- prompt_stage.save()
-
- user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
- name="default-password-change-write"
- )
- login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
- name="default-authentication-login"
- )
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="initial-setup",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={
- "name": "default-oobe-setup",
- "title": "Welcome to authentik!",
- },
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- policy=password_usable_policy, target=flow, defaults={"order": 0}
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=prompt_stage,
- defaults={
- "order": 10,
- },
- )
- user_write_binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=user_write,
- defaults={"order": 20, "evaluate_on_plan": False, "re_evaluate_policies": True},
- )
- PolicyBinding.objects.using(db_alias).update_or_create(
- policy=prefill_policy, target=user_write_binding, defaults={"order": 0}
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=login_stage,
- defaults={
- "order": 100,
- },
- )
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0017_auto_20210329_1334"),
- ("authentik_stages_user_write", "0002_auto_20200918_1653"),
- ("authentik_stages_user_login", "0003_session_duration_delta"),
- ("authentik_stages_password", "0002_passwordstage_change_flow"),
- ("authentik_policies", "0001_initial"),
- ("authentik_policies_expression", "0001_initial"),
]
- operations = [
- migrations.RunPython(create_default_oobe_flow),
- ]
+ operations = []
diff --git a/authentik/flows/migrations/0021_auto_20211227_2103.py b/authentik/flows/migrations/0021_auto_20211227_2103.py
index c1d738aa4..4d300fc4b 100644
--- a/authentik/flows/migrations/0021_auto_20211227_2103.py
+++ b/authentik/flows/migrations/0021_auto_20211227_2103.py
@@ -1,21 +1,5 @@
# Generated by Django 4.0 on 2021-12-27 21:03
-from django.apps.registry import Apps
-from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-
-def update_title_for_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- slug_title_map = {
- "default-provider-authorization-implicit-consent": "Redirecting to %(app)s",
- "default-provider-authorization-explicit-consent": "Redirecting to %(app)s",
- }
- db_alias = schema_editor.connection.alias
- Flow = apps.get_model("authentik_flows", "Flow")
- for flow in Flow.objects.using(db_alias).all():
- if flow.slug not in slug_title_map:
- continue
- flow.title = slug_title_map[flow.slug]
- flow.save()
+from django.db import migrations
class Migration(migrations.Migration):
@@ -24,4 +8,4 @@ class Migration(migrations.Migration):
("authentik_flows", "0020_flowtoken"),
]
- operations = [migrations.RunPython(update_title_for_defaults)]
+ operations = []
diff --git a/authentik/lib/sentry.py b/authentik/lib/sentry.py
index 271b2f2b1..2f9b165df 100644
--- a/authentik/lib/sentry.py
+++ b/authentik/lib/sentry.py
@@ -87,10 +87,6 @@ def sentry_init(**sentry_init_kwargs):
set_tag("authentik.build_hash", get_build_hash("tagged"))
set_tag("authentik.env", get_env())
set_tag("authentik.component", "backend")
- LOGGER.info(
- "Error reporting is enabled",
- env=kwargs["environment"],
- )
def traces_sampler(sampling_context: dict) -> float:
diff --git a/authentik/providers/proxy/migrations/0014_proxy_v2.py b/authentik/providers/proxy/migrations/0014_proxy_v2.py
index 82be52982..2ff0f51df 100644
--- a/authentik/providers/proxy/migrations/0014_proxy_v2.py
+++ b/authentik/providers/proxy/migrations/0014_proxy_v2.py
@@ -1,23 +1,6 @@
# Generated by Django 3.2.6 on 2021-09-09 11:24
-from django.apps.registry import Apps
-from django.core.exceptions import FieldError
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-
-def migrate_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- from authentik.providers.oauth2.models import JWTAlgorithms
- from authentik.providers.proxy.models import ProxyProvider
-
- db_alias = schema_editor.connection.alias
- try:
- for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
- provider.set_oauth_defaults()
- provider.save()
- except FieldError:
- # If the jwt_alg field doesn't exist, just ignore this migration
- pass
class Migration(migrations.Migration):
@@ -26,4 +9,4 @@ class Migration(migrations.Migration):
("authentik_providers_proxy", "0013_mode"),
]
- operations = [migrations.RunPython(migrate_defaults)]
+ operations = []
diff --git a/authentik/sources/saml/migrations/0010_samlsource_pre_authentication_flow.py b/authentik/sources/saml/migrations/0010_samlsource_pre_authentication_flow.py
index 324fe35e0..266ce490c 100644
--- a/authentik/sources/saml/migrations/0010_samlsource_pre_authentication_flow.py
+++ b/authentik/sources/saml/migrations/0010_samlsource_pre_authentication_flow.py
@@ -1,29 +1,7 @@
# Generated by Django 3.1.7 on 2021-03-23 22:09
import django.db.models.deletion
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-
-
-def create_default_pre_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- SAMLSource = apps.get_model("authentik_sources_saml", "samlsource")
-
- db_alias = schema_editor.connection.alias
-
- # Empty flow for providers where consent is implicitly given
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-source-pre-authentication",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={"name": "Pre-Authentication", "title": ""},
- )
-
- for source in SAMLSource.objects.using(db_alias).all():
- source.pre_authentication_flow = flow
- source.save()
class Migration(migrations.Migration):
@@ -47,5 +25,4 @@ class Migration(migrations.Migration):
),
preserve_default=False,
),
- migrations.RunPython(create_default_pre_authentication_flow),
]
diff --git a/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py b/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py
index 7fc920678..49289b12e 100644
--- a/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py
+++ b/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py
@@ -1,42 +1,5 @@
# Generated by Django 3.1.1 on 2020-09-25 14:32
-
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-
-
-def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- AuthenticatorStaticStage = apps.get_model(
- "authentik_stages_authenticator_static", "AuthenticatorStaticStage"
- )
-
- db_alias = schema_editor.connection.alias
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-authenticator-static-setup",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={
- "name": "default-authenticator-static-setup",
- "title": "Setup Static OTP Tokens",
- },
- )
-
- stage, _ = AuthenticatorStaticStage.objects.using(db_alias).update_or_create(
- name="default-authenticator-static-setup", defaults={"token_count": 6}
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=stage, defaults={"order": 0}
- )
-
- for stage in AuthenticatorStaticStage.objects.using(db_alias).filter(configure_flow=None):
- stage.configure_flow = flow
- stage.save()
class Migration(migrations.Migration):
@@ -48,6 +11,4 @@ class Migration(migrations.Migration):
),
]
- operations = [
- migrations.RunPython(create_default_setup_flow),
- ]
+ operations = []
diff --git a/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py b/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
index a9af0239f..4a5251c85 100644
--- a/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
+++ b/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
@@ -1,43 +1,5 @@
# Generated by Django 3.1.1 on 2020-09-25 15:36
-
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-from authentik.stages.authenticator_totp.models import TOTPDigits
-
-
-def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- AuthenticatorTOTPStage = apps.get_model(
- "authentik_stages_authenticator_totp", "AuthenticatorTOTPStage"
- )
-
- db_alias = schema_editor.connection.alias
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-authenticator-totp-setup",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={
- "name": "default-authenticator-totp-setup",
- "title": "Setup Two-Factor authentication",
- },
- )
-
- stage, _ = AuthenticatorTOTPStage.objects.using(db_alias).update_or_create(
- name="default-authenticator-totp-setup", defaults={"digits": TOTPDigits.SIX}
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=stage, defaults={"order": 0}
- )
-
- for stage in AuthenticatorTOTPStage.objects.using(db_alias).filter(configure_flow=None):
- stage.configure_flow = flow
- stage.save()
class Migration(migrations.Migration):
@@ -49,6 +11,4 @@ class Migration(migrations.Migration):
),
]
- operations = [
- migrations.RunPython(create_default_setup_flow),
- ]
+ operations = []
diff --git a/authentik/stages/authenticator_validate/migrations/0009_default_stage.py b/authentik/stages/authenticator_validate/migrations/0009_default_stage.py
index df1d9b569..e963ea896 100644
--- a/authentik/stages/authenticator_validate/migrations/0009_default_stage.py
+++ b/authentik/stages/authenticator_validate/migrations/0009_default_stage.py
@@ -1,57 +1,15 @@
# Generated by Django 3.0.3 on 2020-05-08 14:30
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.stages.authenticator_validate.models import default_device_classes
-
-
-def create_default_validate_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- AuthenticatorValidateStage = apps.get_model(
- "authentik_stages_authenticator_validate", "AuthenticatorValidateStage"
- )
-
- db_alias = schema_editor.connection.alias
-
- auth_flows = Flow.objects.using(db_alias).filter(slug="default-authentication-flow")
- if not auth_flows.exists():
- return
-
- # If there's already a validation stage in the flow, skip
- if (
- AuthenticatorValidateStage.objects.using(db_alias)
- .filter(flow__slug="default-authentication-flow")
- .exists()
- ):
- return
-
- validate_stage, _ = AuthenticatorValidateStage.objects.using(db_alias).update_or_create(
- name="default-authentication-mfa-validation",
- defaults={
- "device_classes": default_device_classes,
- },
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=auth_flows.first(),
- stage=validate_stage,
- defaults={
- "order": 30,
- },
- )
class Migration(migrations.Migration):
dependencies = [
- ("authentik_flows", "0008_default_flows"),
(
"authentik_stages_authenticator_validate",
"0008_alter_authenticatorvalidatestage_device_classes",
),
]
- operations = [migrations.RunPython(create_default_validate_stage)]
+ operations = []
diff --git a/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
index 446393b68..89ca255b9 100644
--- a/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
+++ b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
@@ -1,42 +1,6 @@
# Generated by Django 3.1.1 on 2020-09-25 14:32
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-
-
-def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- AuthenticateWebAuthnStage = apps.get_model(
- "authentik_stages_authenticator_webauthn", "AuthenticateWebAuthnStage"
- )
-
- db_alias = schema_editor.connection.alias
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-authenticator-webauthn-setup",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={
- "name": "default-authenticator-webauthn-setup",
- "title": "Setup WebAuthn",
- },
- )
-
- stage, _ = AuthenticateWebAuthnStage.objects.using(db_alias).update_or_create(
- name="default-authenticator-webauthn-setup"
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=stage, defaults={"order": 0}
- )
-
- for stage in AuthenticateWebAuthnStage.objects.using(db_alias).filter(configure_flow=None):
- stage.configure_flow = flow
- stage.save()
class Migration(migrations.Migration):
@@ -48,6 +12,4 @@ class Migration(migrations.Migration):
),
]
- operations = [
- migrations.RunPython(create_default_setup_flow),
- ]
+ operations = []
diff --git a/authentik/stages/password/migrations/0002_passwordstage_change_flow.py b/authentik/stages/password/migrations/0002_passwordstage_change_flow.py
index 73686f08a..67b0cbba6 100644
--- a/authentik/stages/password/migrations/0002_passwordstage_change_flow.py
+++ b/authentik/stages/password/migrations/0002_passwordstage_change_flow.py
@@ -1,95 +1,13 @@
# Generated by Django 3.0.7 on 2020-06-29 08:51
import django.db.models.deletion
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-from authentik.stages.prompt.models import FieldTypes
-
-
-def create_default_password_change(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
- Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
-
- UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
-
- db_alias = schema_editor.connection.alias
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-password-change",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={"name": "Change Password"},
- )
-
- prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
- name="default-password-change-prompt",
- )
- password_prompt, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="password",
- defaults={
- "label": "Password",
- "type": FieldTypes.PASSWORD,
- "required": True,
- "placeholder": "Password",
- "order": 300,
- },
- )
- password_rep_prompt, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="password_repeat",
- defaults={
- "label": "Password (repeat)",
- "type": FieldTypes.PASSWORD,
- "required": True,
- "placeholder": "Password (repeat)",
- "order": 301,
- },
- )
-
- prompt_stage.fields.add(password_prompt)
- prompt_stage.fields.add(password_rep_prompt)
- prompt_stage.save()
-
- user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
- name="default-password-change-write"
- )
-
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=prompt_stage, defaults={"order": 0}
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow, stage=user_write, defaults={"order": 1}
- )
-
-
-def update_default_stage_change(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- PasswordStage = apps.get_model("authentik_stages_password", "PasswordStage")
- Flow = apps.get_model("authentik_flows", "Flow")
-
- flow = Flow.objects.get(
- slug="default-password-change",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- )
-
- stages = PasswordStage.objects.filter(name="default-authentication-password")
- if not stages.exists():
- return
- stage = stages.first()
- stage.change_flow = flow
- stage.save()
class Migration(migrations.Migration):
dependencies = [
- ("authentik_flows", "0008_default_flows"),
("authentik_stages_password", "0001_initial"),
- ("authentik_stages_prompt", "0001_initial"),
- ("authentik_stages_user_write", "0001_initial"),
]
operations = [
@@ -104,6 +22,4 @@ class Migration(migrations.Migration):
to="authentik_flows.Flow",
),
),
- migrations.RunPython(create_default_password_change),
- migrations.RunPython(update_default_stage_change),
]
diff --git a/authentik/stages/password/migrations/0006_passwordchange_rename.py b/authentik/stages/password/migrations/0006_passwordchange_rename.py
index 98a9c7ee5..f1de27891 100644
--- a/authentik/stages/password/migrations/0006_passwordchange_rename.py
+++ b/authentik/stages/password/migrations/0006_passwordchange_rename.py
@@ -1,21 +1,5 @@
# Generated by Django 3.2.5 on 2021-08-21 13:12
-from django.apps.registry import Apps
from django.db import migrations
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-
-def rename_default_prompt_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
- db_alias = schema_editor.connection.alias
-
- stages = PromptStage.objects.using(db_alias).filter(name="Change your password")
- if not stages.exists():
- return
- stage = stages.first()
- if PromptStage.objects.using(db_alias).filter(name="default-password-change-prompt").exists():
- return
- stage.name = "default-password-change-prompt"
- stage.save()
class Migration(migrations.Migration):
@@ -24,6 +8,4 @@ class Migration(migrations.Migration):
("authentik_stages_password", "0005_auto_20210402_2221"),
]
- operations = [
- migrations.RunPython(rename_default_prompt_stage),
- ]
+ operations = []
diff --git a/authentik/tenants/migrations/0002_tenant_flow_user_settings.py b/authentik/tenants/migrations/0002_tenant_flow_user_settings.py
index 93e54dbf3..e09dc6cdc 100644
--- a/authentik/tenants/migrations/0002_tenant_flow_user_settings.py
+++ b/authentik/tenants/migrations/0002_tenant_flow_user_settings.py
@@ -1,160 +1,7 @@
# Generated by Django 4.0.2 on 2022-02-26 21:14
import django.db.models.deletion
-from django.apps.registry import Apps
from django.db import migrations, models
-from django.db.backends.base.schema import BaseDatabaseSchemaEditor
-
-from authentik.flows.models import FlowDesignation
-from authentik.stages.identification.models import UserFields
-from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
-
-AUTHORIZATION_POLICY = """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
-"""
-
-
-def create_default_user_settings_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
- from authentik.stages.prompt.models import FieldTypes
-
- db_alias = schema_editor.connection.alias
-
- Tenant = apps.get_model("authentik_tenants", "Tenant")
-
- Flow = apps.get_model("authentik_flows", "Flow")
- FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
-
- ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
-
- UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
- PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
- Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
-
- prompt_username, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="username",
- order=200,
- defaults={
- "label": "Username",
- "type": FieldTypes.TEXT,
- "placeholder": """try:
- return user.username
-except:
- return ''""",
- "placeholder_expression": True,
- },
- )
- prompt_name, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="name",
- order=201,
- defaults={
- "label": "Name",
- "type": FieldTypes.TEXT,
- "placeholder": """try:
- return user.name
-except:
- return ''""",
- "placeholder_expression": True,
- },
- )
- prompt_email, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="email",
- order=202,
- defaults={
- "label": "Email",
- "type": FieldTypes.EMAIL,
- "placeholder": """try:
- return user.email
-except:
- return ''""",
- "placeholder_expression": True,
- },
- )
- prompt_locale, _ = Prompt.objects.using(db_alias).update_or_create(
- field_key="attributes.settings.locale",
- order=203,
- defaults={
- "label": "Locale",
- "type": FieldTypes.AK_LOCALE,
- "placeholder": """try:
- return user.attributes.get("settings", {}).get("locale", "")
-except:
- return ''""",
- "placeholder_expression": True,
- "required": True,
- },
- )
-
- validation_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
- name="default-user-settings-authorization",
- defaults={
- "expression": AUTHORIZATION_POLICY,
- },
- )
- prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
- name="default-user-settings",
- )
- prompt_stage.validation_policies.set([validation_policy])
- prompt_stage.fields.set([prompt_username, prompt_name, prompt_email, prompt_locale])
- prompt_stage.save()
- user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
- name="default-user-settings-write"
- )
-
- flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-user-settings-flow",
- designation=FlowDesignation.STAGE_CONFIGURATION,
- defaults={
- "name": "Update your info",
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=prompt_stage,
- defaults={
- "order": 20,
- },
- )
- FlowStageBinding.objects.using(db_alias).update_or_create(
- target=flow,
- stage=user_write,
- defaults={
- "order": 100,
- },
- )
-
- tenant = Tenant.objects.using(db_alias).filter(default=True).first()
- if not tenant:
- return
- tenant.flow_user_settings = flow
- tenant.save()
class Migration(migrations.Migration):
@@ -177,5 +24,4 @@ class Migration(migrations.Migration):
to="authentik_flows.flow",
),
),
- migrations.RunPython(create_default_user_settings_flow),
]
diff --git a/blueprints/default/40-events-default.yaml b/blueprints/default/40-events-default.yaml
new file mode 100644
index 000000000..ad2bdc96d
--- /dev/null
+++ b/blueprints/default/40-events-default.yaml
@@ -0,0 +1,117 @@
+version: 1
+metadata:
+ name: Default - Events Transport & Rules
+entries:
+- model: authentik_events.notificationtransport
+ id: default-email-transport
+ attrs:
+ mode: email
+ identifiers:
+ name: default-email-transport
+- model: authentik_events.notificationtransport
+ id: default-local-transport
+ attrs:
+ mode: local
+ identifiers:
+ name: default-local-transport
+- model: authentik_core.group
+ id: group
+ identifiers:
+ name: authentik Admins
+ attrs:
+ is_superuser: true
+ users: []
+ parent: null
+
+- model: authentik_policies_event_matcher.eventmatcherpolicy
+ id: default-match-configuration-error
+ attrs:
+ action: configuration_error
+ identifiers:
+ name: default-match-configuration-error
+- model: authentik_events.notificationrule
+ id: default-notify-configuration-error
+ identifiers:
+ name: default-notify-configuration-error
+ attrs:
+ severity: alert
+ group: !KeyOf group
+ transports:
+ - !KeyOf default-email-transport
+ - !KeyOf default-local-transport
+- model: authentik_policies.policybinding
+ attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 0
+ policy: !KeyOf default-match-configuration-error
+ target: !KeyOf default-notify-configuration-error
+
+- model: authentik_policies_event_matcher.eventmatcherpolicy
+ id: default-match-update
+ attrs:
+ action: update_available
+ identifiers:
+ name: default-match-update
+- model: authentik_events.notificationrule
+ id: default-notify-update
+ identifiers:
+ name: default-notify-update
+ attrs:
+ severity: alert
+ group: !KeyOf group
+ transports:
+ - !KeyOf default-email-transport
+ - !KeyOf default-local-transport
+- model: authentik_policies.policybinding
+ attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 0
+ policy: !KeyOf default-match-update
+ target: !KeyOf default-notify-update
+
+- model: authentik_policies_event_matcher.eventmatcherpolicy
+ id: default-match-policy-exception
+ attrs:
+ action: policy_exception
+ identifiers:
+ name: default-match-policy-exception
+- model: authentik_policies_event_matcher.eventmatcherpolicy
+ id: default-match-property-mapping-exception
+ attrs:
+ action: property_mapping_exception
+ identifiers:
+ name: default-match-property-mapping-exception
+- model: authentik_events.notificationrule
+ id: default-notify-exception
+ identifiers:
+ name: default-notify-exception
+ attrs:
+ severity: alert
+ group: !KeyOf group
+ transports:
+ - !KeyOf default-email-transport
+ - !KeyOf default-local-transport
+- model: authentik_policies.policybinding
+ attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 0
+ policy: !KeyOf default-match-policy-exception
+ target: !KeyOf default-notify-exception
+- model: authentik_policies.policybinding
+ attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 1
+ policy: !KeyOf default-match-property-mapping-exception
+ target: !KeyOf default-notify-exception
diff --git a/blueprints/default/90-default-tenant.yaml b/blueprints/default/90-default-tenant.yaml
index e3eaf5a1f..c7d0bc16c 100644
--- a/blueprints/default/90-default-tenant.yaml
+++ b/blueprints/default/90-default-tenant.yaml
@@ -5,6 +5,7 @@ entries:
- attrs:
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
+ flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
identifiers:
domain: authentik-default
default: True
diff --git a/blueprints/default/91-flow-oobe.yaml b/blueprints/default/91-flow-oobe.yaml
new file mode 100644
index 000000000..ef2b44d6c
--- /dev/null
+++ b/blueprints/default/91-flow-oobe.yaml
@@ -0,0 +1,161 @@
+metadata:
+ name: Default - Out-of-box-experience flow
+version: 1
+entries:
+- attrs:
+ compatibility_mode: false
+ denied_action: message_continue
+ designation: stage_configuration
+ name: default-oobe-setup
+ policy_engine_mode: all
+ title: Welcome to authentik!
+ id: flow
+ identifiers:
+ slug: initial-setup
+ model: authentik_flows.flow
+- attrs:
+ order: 100
+ placeholder: Welcome to authentik! Please set a password for the default admin
+ user, akadmin.
+ placeholder_expression: false
+ required: true
+ sub_text: ''
+ type: static
+ id: prompt-field-header
+ identifiers:
+ field_key: oobe-header-text
+ label: oobe-header-text
+ model: authentik_stages_prompt.prompt
+- attrs:
+ order: 101
+ placeholder: Admin email
+ placeholder_expression: false
+ required: true
+ sub_text: ''
+ type: email
+ id: prompt-field-email
+ identifiers:
+ field_key: email
+ label: Email
+ model: authentik_stages_prompt.prompt
+- attrs:
+ order: 300
+ placeholder: Password
+ placeholder_expression: false
+ required: true
+ sub_text: ''
+ type: password
+ id: prompt-field-password
+ identifiers:
+ field_key: password
+ label: Password
+ model: authentik_stages_prompt.prompt
+- attrs:
+ order: 301
+ placeholder: Password (repeat)
+ placeholder_expression: false
+ required: true
+ sub_text: ''
+ type: password
+ id: prompt-field-password-repeat
+ identifiers:
+ field_key: password_repeat
+ label: Password (repeat)
+ model: authentik_stages_prompt.prompt
+- attrs:
+ execution_logging: false
+ expression: |
+ # This policy sets the user for the currently running flow
+ # by injecting "pending_user"
+ akadmin = ak_user_by(username="akadmin")
+ context["flow_plan"].context["pending_user"] = akadmin
+ return True
+ id: policy-default-oobe-prefill-user
+ identifiers:
+ name: default-oobe-prefill-user
+ model: authentik_policies_expression.expressionpolicy
+- attrs:
+ execution_logging: false
+ expression: |
+ # This policy ensures that the setup flow can only be
+ # executed when the admin user doesn''t have a password set
+ akadmin = ak_user_by(username="akadmin")
+ return not akadmin.has_usable_password()
+ id: policy-default-oobe-password-usable
+ identifiers:
+ name: default-oobe-password-usable
+ model: authentik_policies_expression.expressionpolicy
+- attrs:
+ fields:
+ - !KeyOf prompt-field-header
+ - !KeyOf prompt-field-email
+ - !KeyOf prompt-field-password
+ - !KeyOf prompt-field-password-repeat
+ validation_policies: []
+ id: stage-default-oobe-password
+ identifiers:
+ name: stage-default-oobe-password
+ model: authentik_stages_prompt.promptstage
+- attrs:
+ session_duration: seconds=0
+ id: stage-default-authentication-login
+ identifiers:
+ name: default-authentication-login
+ model: authentik_stages_user_login.userloginstage
+- attrs:
+ create_users_as_inactive: false
+ create_users_group: null
+ user_path_template: ''
+ id: stage-default-password-change-write
+ identifiers:
+ name: 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: 10
+ stage: !KeyOf stage-default-oobe-password
+ target: !KeyOf flow
+ model: authentik_flows.flowstagebinding
+- attrs:
+ evaluate_on_plan: false
+ invalid_response_action: retry
+ policy_engine_mode: all
+ re_evaluate_policies: true
+ id: binding-password-write
+ identifiers:
+ order: 20
+ stage: !KeyOf stage-default-password-change-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: 100
+ stage: !KeyOf stage-default-authentication-login
+ target: !KeyOf flow
+ model: authentik_flows.flowstagebinding
+- attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 0
+ policy: !KeyOf policy-default-oobe-password-usable
+ target: !KeyOf flow
+ model: authentik_policies.policybinding
+- attrs:
+ enabled: true
+ negate: false
+ timeout: 30
+ identifiers:
+ order: 0
+ policy: !KeyOf policy-default-oobe-prefill-user
+ target: !KeyOf binding-password-write
+ model: authentik_policies.policybinding
diff --git a/blueprints/schema.json b/blueprints/schema.json
new file mode 100644
index 000000000..f4c17bb46
--- /dev/null
+++ b/blueprints/schema.json
@@ -0,0 +1,162 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/example.json",
+ "type": "object",
+ "title": "authentik Blueprint schema",
+ "default": {},
+ "required": [
+ "version",
+ "entries"
+ ],
+ "properties": {
+ "version": {
+ "$id": "#/properties/version",
+ "type": "integer",
+ "title": "Blueprint version",
+ "default": 1
+ },
+ "metadata": {
+ "$id": "#/properties/metadata",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "labels": {
+ "type": "object"
+ }
+ }
+ },
+ "entries": {
+ "type": "array",
+ "items": {
+ "$id": "#entry",
+ "type": "object",
+ "required": [
+ "model",
+ "identifiers"
+ ],
+ "properties": {
+ "model": {
+ "type": "string",
+ "enum": [
+ "auth.permission",
+ "contenttypes.contenttype",
+ "sessions.session",
+ "authentik_crypto.certificatekeypair",
+ "authentik_events.event",
+ "authentik_events.notificationtransport",
+ "authentik_events.notification",
+ "authentik_events.notificationrule",
+ "authentik_events.notificationwebhookmapping",
+ "authentik_flows.flow",
+ "authentik_flows.flowstagebinding",
+ "authentik_flows.flowtoken",
+ "authentik_outposts.dockerserviceconnection",
+ "authentik_outposts.kubernetesserviceconnection",
+ "authentik_outposts.outpost",
+ "authentik_policies_dummy.dummypolicy",
+ "authentik_policies_event_matcher.eventmatcherpolicy",
+ "authentik_policies_expiry.passwordexpirypolicy",
+ "authentik_policies_expression.expressionpolicy",
+ "authentik_policies_hibp.haveibeenpwendpolicy",
+ "authentik_policies_password.passwordpolicy",
+ "authentik_policies_reputation.reputationpolicy",
+ "authentik_policies_reputation.reputation",
+ "authentik_policies.policybinding",
+ "authentik_providers_ldap.ldapprovider",
+ "authentik_providers_oauth2.scopemapping",
+ "authentik_providers_oauth2.oauth2provider",
+ "authentik_providers_oauth2.authorizationcode",
+ "authentik_providers_oauth2.refreshtoken",
+ "authentik_providers_proxy.proxyprovider",
+ "authentik_providers_saml.samlprovider",
+ "authentik_providers_saml.samlpropertymapping",
+ "authentik_sources_ldap.ldapsource",
+ "authentik_sources_ldap.ldappropertymapping",
+ "authentik_sources_oauth.oauthsource",
+ "authentik_sources_oauth.useroauthsourceconnection",
+ "authentik_sources_plex.plexsource",
+ "authentik_sources_plex.plexsourceconnection",
+ "authentik_sources_saml.samlsource",
+ "authentik_stages_authenticator_duo.authenticatorduostage",
+ "authentik_stages_authenticator_duo.duodevice",
+ "authentik_stages_authenticator_sms.authenticatorsmsstage",
+ "authentik_stages_authenticator_sms.smsdevice",
+ "authentik_stages_authenticator_static.authenticatorstaticstage",
+ "authentik_stages_authenticator_totp.authenticatortotpstage",
+ "authentik_stages_authenticator_validate.authenticatorvalidatestage",
+ "authentik_stages_authenticator_webauthn.authenticatewebauthnstage",
+ "authentik_stages_authenticator_webauthn.webauthndevice",
+ "authentik_stages_captcha.captchastage",
+ "authentik_stages_consent.consentstage",
+ "authentik_stages_consent.userconsent",
+ "authentik_stages_deny.denystage",
+ "authentik_stages_dummy.dummystage",
+ "authentik_stages_email.emailstage",
+ "authentik_stages_identification.identificationstage",
+ "authentik_stages_invitation.invitationstage",
+ "authentik_stages_invitation.invitation",
+ "authentik_stages_password.passwordstage",
+ "authentik_stages_prompt.prompt",
+ "authentik_stages_prompt.promptstage",
+ "authentik_stages_user_delete.userdeletestage",
+ "authentik_stages_user_login.userloginstage",
+ "authentik_stages_user_logout.userlogoutstage",
+ "authentik_stages_user_write.userwritestage",
+ "authentik_tenants.tenant",
+ "authentik_blueprints.blueprintinstance",
+ "guardian.userobjectpermission",
+ "guardian.groupobjectpermission",
+ "otp_static.staticdevice",
+ "otp_static.statictoken",
+ "otp_totp.totpdevice",
+ "silk.request",
+ "silk.response",
+ "silk.sqlquery",
+ "silk.profile",
+ "authentik_core.group",
+ "authentik_core.user",
+ "authentik_core.application",
+ "authentik_core.token"
+ ]
+ },
+ "id": {
+ "type": "string"
+ },
+ "attrs": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Commonly available field, may not exist on all models"
+ }
+ },
+ "additionalProperties": true
+ },
+ "identifiers": {
+ "type": "object",
+ "properties": {
+ "pk": {
+ "description": "Commonly available field, may not exist on all models",
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "format": "uuid"
+ }
+ ]
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lifecycle/ak b/lifecycle/ak
index c8666e633..1dbbb3493 100755
--- a/lifecycle/ak
+++ b/lifecycle/ak
@@ -48,7 +48,7 @@ elif [[ "$1" == "worker" ]]; then
check_if_root "celery -A authentik.root.celery worker -Ofair --max-tasks-per-child=1 --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events"
elif [[ "$1" == "bash" ]]; then
/bin/bash
-elif [[ "$1" == "test" ]]; then
+elif [[ "$1" == "test-all" ]]; then
pip install --no-cache-dir -r /requirements-dev.txt
touch /unittest.xml
chown authentik:authentik /unittest.xml
diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts
index 6480a9e0e..aae1e035a 100644
--- a/web/src/interfaces/AdminInterface.ts
+++ b/web/src/interfaces/AdminInterface.ts
@@ -218,9 +218,6 @@ export class AdminInterface extends LitElement {
${t`Outposts`}
-
- ${t`Outpost Integrations`}
-
${t`Events`}
@@ -248,6 +245,9 @@ export class AdminInterface extends LitElement {
${t`Property Mappings`}
+
+ ${t`Blueprints`}
+
${t`Flows & Stages`}
@@ -300,8 +300,8 @@ export class AdminInterface extends LitElement {
${t`Certificates`}
-
- ${t`Blueprints`}
+
+ ${t`Outpost Integrations`}
`;
diff --git a/web/src/pages/blueprints/BlueprintForm.ts b/web/src/pages/blueprints/BlueprintForm.ts
index cbc8ba80e..acb85a9e1 100644
--- a/web/src/pages/blueprints/BlueprintForm.ts
+++ b/web/src/pages/blueprints/BlueprintForm.ts
@@ -10,6 +10,7 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { BlueprintInstance, ManagedApi } from "@goauthentik/api";
@@ -48,7 +49,7 @@ export class BlueprintForm extends ModelForm {