From ec42d378abf81d5d3b281436c44ac37db97f6ff5 Mon Sep 17 00:00:00 2001 From: Jens L Date: Fri, 5 Aug 2022 08:39:00 +0200 Subject: [PATCH] blueprints/cleanup (#3369) --- .github/workflows/release-tag.yml | 2 +- .vscode/settings.json | 5 +- Makefile | 7 +- authentik/blueprints/api.py | 3 +- .../commands/make_blueprint_schema.py | 35 ++++ .../management/commands/schema_template.json | 84 +++++++++ .../blueprints/migrations/0001_initial.py | 5 +- authentik/blueprints/tests/test_models.py | 4 +- authentik/blueprints/tests/test_v1_api.py | 45 +++++ authentik/blueprints/v1/importer.py | 41 +++-- authentik/blueprints/v1/tasks.py | 14 +- .../0002_alter_notificationtransport_mode.py | 25 --- .../0011_notification_rules_default_v1.py | 134 +-------------- authentik/events/utils.py | 3 + .../flows/migrations/0008_default_flows.py | 93 +--------- .../flows/migrations/0009_source_flows.py | 141 +-------------- .../flows/migrations/0010_provider_flows.py | 36 +--- authentik/flows/migrations/0011_flow_title.py | 23 --- .../migrations/0014_auto_20200925_2332.py | 22 --- authentik/flows/migrations/0018_oob_flows.py | 131 +------------- .../migrations/0021_auto_20211227_2103.py | 20 +-- authentik/lib/sentry.py | 4 - .../proxy/migrations/0014_proxy_v2.py | 19 +- ...0010_samlsource_pre_authentication_flow.py | 23 --- .../migrations/0005_default_setup_flow.py | 41 +---- .../migrations/0006_default_setup_flow.py | 42 +---- .../migrations/0009_default_stage.py | 44 +---- .../migrations/0002_default_setup_flow.py | 40 +---- .../0002_passwordstage_change_flow.py | 84 --------- .../migrations/0006_passwordchange_rename.py | 20 +-- .../0002_tenant_flow_user_settings.py | 154 ----------------- blueprints/default/40-events-default.yaml | 117 +++++++++++++ blueprints/default/90-default-tenant.yaml | 1 + blueprints/default/91-flow-oobe.yaml | 161 +++++++++++++++++ blueprints/schema.json | 162 ++++++++++++++++++ lifecycle/ak | 2 +- web/src/interfaces/AdminInterface.ts | 10 +- web/src/pages/blueprints/BlueprintForm.ts | 3 +- 38 files changed, 679 insertions(+), 1121 deletions(-) create mode 100644 authentik/blueprints/management/commands/make_blueprint_schema.py create mode 100644 authentik/blueprints/management/commands/schema_template.json create mode 100644 authentik/blueprints/tests/test_v1_api.py create mode 100644 blueprints/default/40-events-default.yaml create mode 100644 blueprints/default/91-flow-oobe.yaml create mode 100644 blueprints/schema.json 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 {