blueprints/cleanup (#3369)

This commit is contained in:
Jens L 2022-08-05 08:39:00 +02:00 committed by GitHub
parent 2a5a975d9a
commit ec42d378ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 679 additions and 1121 deletions

View File

@ -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

View File

@ -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"
}
}

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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
}
}
}
}
}
}

View File

@ -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()

View File

@ -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))

View File

@ -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"
),
)

View File

@ -35,7 +35,16 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
EXCLUDED_MODELS = (
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,
@ -48,6 +57,7 @@ EXCLUDED_MODELS = (
# 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()

View File

@ -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:

View File

@ -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),
]

View File

@ -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 = []

View File

@ -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__,

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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",

View File

@ -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),
]

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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:

View File

@ -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 = []

View File

@ -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),
]

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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 = []

View File

@ -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),
]

View File

@ -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 = []

View File

@ -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),
]

View File

@ -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

View File

@ -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

View File

@ -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

162
blueprints/schema.json Normal file
View File

@ -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
}
}
}
}
}
}

View File

@ -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

View File

@ -218,9 +218,6 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/outpost/outposts">
<span slot="label">${t`Outposts`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/outpost/integrations">
<span slot="label">${t`Outpost Integrations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`Events`}</span>
@ -248,6 +245,9 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/core/property-mappings">
<span slot="label">${t`Property Mappings`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/blueprints/instances">
<span slot="label">${t`Blueprints`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`Flows & Stages`}</span>
@ -300,8 +300,8 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/crypto/certificates">
<span slot="label">${t`Certificates`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/blueprints/instances">
<span slot="label">${t`Blueprints`}</span>
<ak-sidebar-item path="/outpost/integrations">
<span slot="label">${t`Outpost Integrations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`;

View File

@ -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<BlueprintInstance, string> {
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name)}"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>