core: complete rework to oobe and setup experience (#21753)

* initial

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use same startup template

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix check not working

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unrelated: fix inspector auth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ensure oobe flow can only accessed via correct url

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set setup flag when applying bootstrap blueprint when env is set

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add system visibility to flags to make them non-editable

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set setup flag for e2e tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests and linting

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make github lint happy

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make tests have less assumptions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update docs

* include more heuristics in migration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add management command to set any flag

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate worker command to signal

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improved api for setting flags

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* short circuit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
This commit is contained in:
Jens L.
2026-04-24 13:47:05 +01:00
committed by GitHub
parent 0459568a96
commit c6ee7b6881
24 changed files with 526 additions and 67 deletions

View File

@@ -1,5 +1,6 @@
"""Apply blueprint from commandline"""
from argparse import ArgumentParser
from sys import exit as sys_exit
from django.core.management.base import BaseCommand, no_translations
@@ -31,5 +32,5 @@ class Command(BaseCommand):
sys_exit(1)
importer.apply()
def add_arguments(self, parser):
def add_arguments(self, parser: ArgumentParser):
parser.add_argument("blueprints", nargs="+", type=str)

View File

@@ -7,6 +7,12 @@ from authentik.tasks.schedules.common import ScheduleSpec
from authentik.tenants.flags import Flag
class Setup(Flag[bool], key="setup"):
default = False
visibility = "system"
class AppAccessWithoutBindings(Flag[bool], key="core_default_app_access"):
default = True
@@ -26,6 +32,10 @@ class AuthentikCoreConfig(ManagedAppConfig):
mountpoint = ""
default = True
def import_related(self):
super().import_related()
self.import_module("authentik.core.setup.signals")
@ManagedAppConfig.reconcile_tenant
def source_inbuilt(self):
"""Reconcile inbuilt source"""

View File

@@ -0,0 +1,61 @@
# Generated by Django 5.2.13 on 2026-04-21 18:49
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db import migrations
def check_is_already_setup(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.conf import settings
from authentik.flows.models import FlowAuthenticationRequirement
VersionHistory = apps.get_model("authentik_admin", "VersionHistory")
Flow = apps.get_model("authentik_flows", "Flow")
User = apps.get_model("authentik_core", "User")
db_alias = schema_editor.connection.alias
# Upgrading from a previous version
if not settings.TEST and VersionHistory.objects.using(db_alias).count() > 1:
return True
# OOBE flow sets itself to this authentication requirement once finished
if (
Flow.objects.using(db_alias)
.filter(
slug="initial-setup", authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER
)
.exists()
):
return True
# non-akadmin and non-guardian anonymous user exist
if (
User.objects.using(db_alias)
.exclude(username="akadmin")
.exclude(username="AnonymousUser")
.exists()
):
return True
return False
def update_setup_flag(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.core.apps import Setup
from authentik.tenants.utils import get_current_tenant
is_already_setup = check_is_already_setup(apps, schema_editor)
if is_already_setup:
tenant = get_current_tenant()
tenant.flags[Setup().key] = True
tenant.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
# 0024_flow_authentication adds the `authentication` field.
("authentik_flows", "0024_flow_authentication"),
]
operations = [migrations.RunPython(update_setup_flag, migrations.RunPython.noop)]

View File

View File

@@ -0,0 +1,38 @@
from os import getenv
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.core.apps import Setup
from authentik.root.signals import post_startup
from authentik.tenants.models import Tenant
BOOTSTRAP_BLUEPRINT = "system/bootstrap.yaml"
LOGGER = get_logger()
@receiver(post_startup)
def post_startup_setup_bootstrap(sender, **_):
if not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD") and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN"):
return
LOGGER.info("Configuring authentik through bootstrap environment variables")
content = BlueprintInstance(path=BOOTSTRAP_BLUEPRINT).retrieve()
# If we have bootstrap credentials set, run bootstrap tasks outside of main server
# sync, so that we can sure the first start actually has working bootstrap
# credentials
for tenant in Tenant.objects.filter(ready=True):
if Setup.get(tenant=tenant):
LOGGER.info("Tenant is already setup, skipping", tenant=tenant.schema_name)
continue
with tenant:
importer = Importer.from_string(content)
valid, logs = importer.validate()
if not valid:
LOGGER.warning("Blueprint invalid", tenant=tenant.schema_name)
for log in logs:
log.log()
importer.apply()
Setup.set(True, tenant=tenant)

View File

@@ -0,0 +1,80 @@
from functools import lru_cache
from http import HTTPMethod, HTTPStatus
from django.contrib.staticfiles import finders
from django.db import transaction
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.views import View
from structlog.stdlib import get_logger
from authentik.blueprints.models import BlueprintInstance
from authentik.core.apps import Setup
from authentik.flows.models import Flow, FlowAuthenticationRequirement, in_memory_stage
from authentik.flows.planner import FlowPlanner
from authentik.flows.stage import StageView
LOGGER = get_logger()
FLOW_CONTEXT_START_BY = "goauthentik.io/core/setup/started-by"
@lru_cache
def read_static(path: str) -> str | None:
result = finders.find(path)
if not result:
return None
with open(result, encoding="utf8") as _file:
return _file.read()
class SetupView(View):
setup_flow_slug = "initial-setup"
def dispatch(self, request: HttpRequest, *args, **kwargs):
if request.method != HTTPMethod.HEAD and Setup.get():
return redirect(reverse("authentik_core:root-redirect"))
return super().dispatch(request, *args, **kwargs)
def head(self, request: HttpRequest, *args, **kwargs):
if Setup.get():
return HttpResponse(status=HTTPStatus.SERVICE_UNAVAILABLE)
if not Flow.objects.filter(slug=self.setup_flow_slug).exists():
return HttpResponse(status=HTTPStatus.SERVICE_UNAVAILABLE)
return HttpResponse(status=HTTPStatus.OK)
def get(self, request: HttpRequest):
flow = Flow.objects.filter(slug=self.setup_flow_slug).first()
if not flow:
LOGGER.info("Setup flow does not exist yet, waiting for worker to finish")
return HttpResponse(
read_static("dist/standalone/loading/startup.html"),
status=HTTPStatus.SERVICE_UNAVAILABLE,
)
planner = FlowPlanner(flow)
plan = planner.plan(request, {FLOW_CONTEXT_START_BY: "setup"})
plan.append_stage(in_memory_stage(PostSetupStageView))
return plan.to_redirect(request, flow)
class PostSetupStageView(StageView):
"""Run post-setup tasks"""
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Wrapper when this stage gets hit with a post request"""
return self.get(request, *args, **kwargs)
def get(self, requeset: HttpRequest, *args, **kwargs):
with transaction.atomic():
# Remember we're setup
Setup.set(True)
# Disable OOBE Blueprints
BlueprintInstance.objects.filter(
**{"metadata__labels__blueprints.goauthentik.io/system-oobe": "true"}
).update(enabled=False)
# Make flow inaccessible
Flow.objects.filter(slug="initial-setup").update(
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER
)
return self.executor.stage_ok()

View File

@@ -4,6 +4,7 @@ from django.test import TestCase
from django.urls import reverse
from authentik.brands.models import Brand
from authentik.core.apps import Setup
from authentik.core.models import Application, UserTypes
from authentik.core.tests.utils import create_test_brand, create_test_user
@@ -12,6 +13,7 @@ class TestInterfaceRedirects(TestCase):
"""Test RootRedirectView and BrandDefaultRedirectView redirect logic by user type"""
def setUp(self):
Setup.set(True)
self.app = Application.objects.create(name="test-app", slug="test-app")
self.brand: Brand = create_test_brand(default_application=self.app)

View File

@@ -0,0 +1,156 @@
from http import HTTPStatus
from os import environ
from django.urls import reverse
from authentik.blueprints.tests import apply_blueprint
from authentik.core.apps import Setup
from authentik.core.models import Token, TokenIntents, User
from authentik.flows.models import Flow
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.root.signals import post_startup, pre_startup
from authentik.tenants.flags import patch_flag
class TestSetup(FlowTestCase):
def tearDown(self):
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD", None)
environ.pop("AUTHENTIK_BOOTSTRAP_TOKEN", None)
@patch_flag(Setup, True)
def test_setup(self):
"""Test existing instance"""
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_flows:default-authentication") + "?next=/",
fetch_redirect_response=False,
)
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:root-redirect"),
fetch_redirect_response=False,
)
@patch_flag(Setup, False)
def test_not_setup_no_flow(self):
"""Test case on initial startup; setup flag is not set and oobe flow does
not exist yet"""
Flow.objects.filter(slug="initial-setup").delete()
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(res, reverse("authentik_core:setup"), fetch_redirect_response=False)
# Flow does not exist, hence 503
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
@patch_flag(Setup, False)
@apply_blueprint("default/flow-oobe.yaml")
def test_not_setup(self):
"""Test case for when worker comes up, and has created flow"""
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(res, reverse("authentik_core:setup"), fetch_redirect_response=False)
# Flow does not exist, hence 503
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.OK)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:if-flow", kwargs={"flow_slug": "initial-setup"}),
fetch_redirect_response=False,
)
@apply_blueprint("default/flow-oobe.yaml")
@apply_blueprint("system/bootstrap.yaml")
def test_setup_flow_full(self):
"""Test full setup flow"""
Setup.set(False)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:if-flow", kwargs={"flow_slug": "initial-setup"}),
fetch_redirect_response=False,
)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertStageResponse(res, component="ak-stage-prompt")
pw = generate_id()
res = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
{
"email": f"{generate_id()}@t.goauthentik.io",
"password": pw,
"password_repeat": pw,
"component": "ak-stage-prompt",
},
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertTrue(user.check_password(pw))
@patch_flag(Setup, False)
@apply_blueprint("default/flow-oobe.yaml")
@apply_blueprint("system/bootstrap.yaml")
def test_setup_flow_direct(self):
"""Test setup flow, directly accessing the flow"""
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"})
)
self.assertStageResponse(
res,
component="ak-stage-access-denied",
error_message="Access the authentik setup by navigating to http://testserver/",
)
def test_setup_bootstrap_env(self):
"""Test setup with env vars"""
User.objects.filter(username="akadmin").delete()
Setup.set(False)
environ["AUTHENTIK_BOOTSTRAP_PASSWORD"] = generate_id()
environ["AUTHENTIK_BOOTSTRAP_TOKEN"] = generate_id()
pre_startup.send(sender=self)
post_startup.send(sender=self)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertTrue(user.check_password(environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]))
token = Token.objects.filter(identifier="authentik-bootstrap-token").first()
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.key, environ["AUTHENTIK_BOOTSTRAP_TOKEN"])

View File

@@ -1,7 +1,6 @@
"""authentik URL Configuration"""
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
@@ -19,6 +18,7 @@ from authentik.core.api.sources import (
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
from authentik.core.api.users import UserViewSet
from authentik.core.setup.views import SetupView
from authentik.core.views.apps import RedirectToAppLaunch
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import (
@@ -35,7 +35,7 @@ from authentik.tenants.channels import TenantsAwareMiddleware
urlpatterns = [
path(
"",
login_required(RootRedirectView.as_view()),
RootRedirectView.as_view(),
name="root-redirect",
),
path(
@@ -62,6 +62,11 @@ urlpatterns = [
FlowInterfaceView.as_view(),
name="if-flow",
),
path(
"setup",
SetupView.as_view(),
name="setup",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
path(

View File

@@ -3,6 +3,7 @@
from json import dumps
from typing import Any
from django.contrib.auth.mixins import AccessMixin
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import redirect
@@ -14,12 +15,13 @@ from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.apps import Setup
from authentik.core.models import UserTypes
from authentik.lib.config import CONFIG
from authentik.policies.denied import AccessDeniedResponse
class RootRedirectView(RedirectView):
class RootRedirectView(AccessMixin, RedirectView):
"""Root redirect view, redirect to brand's default application if set"""
pattern_name = "authentik_core:if-user"
@@ -40,6 +42,10 @@ class RootRedirectView(RedirectView):
return None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if not Setup.get():
return redirect("authentik_core:setup")
if not request.user.is_authenticated:
return self.handle_no_permission()
if redirect_response := RootRedirectView().redirect_to_app(request):
return redirect_response
return super().dispatch(request, *args, **kwargs)

View File

@@ -71,7 +71,11 @@ class FlowInspectorView(APIView):
flow: Flow
_logger: BoundLogger
permission_classes = [IsAuthenticated]
def get_permissions(self):
if settings.DEBUG:
return []
return [IsAuthenticated()]
def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug)

View File

@@ -1,5 +1,7 @@
"""authentik recovery create_admin_group"""
from argparse import ArgumentParser
from django.utils.translation import gettext as _
from authentik.core.models import User
@@ -12,7 +14,7 @@ class Command(TenantCommand):
help = _("Create admin group if the default group gets deleted.")
def add_arguments(self, parser):
def add_arguments(self, parser: ArgumentParser):
parser.add_argument("user", action="store", help="User to add to the admin group.")
def handle_per_tenant(self, *args, **options):

View File

@@ -19,19 +19,32 @@ from authentik.tenants.models import Tenant
class FlagJSONField(JSONDictField):
def to_representation(self, value: dict) -> dict:
"""Exclude any system flags that aren't modifiable"""
new_value = value.copy()
for flag in Flag.available(exclude_system=False):
_flag = flag()
if _flag.visibility == "system":
new_value.pop(_flag.key, None)
return super().to_representation(new_value)
def run_validators(self, value: dict):
super().run_validators(value)
for flag in Flag.available():
for flag in Flag.available(exclude_system=False):
_flag = flag()
if _flag.key in value:
flag_value = value.get(_flag.key)
flag_type = get_args(_flag.__orig_bases__[0])[0]
if flag_value and not isinstance(flag_value, flag_type):
raise ValidationError(
_("Value for flag {flag_key} needs to be of type {type}.").format(
flag_key=_flag.key, type=flag_type.__name__
)
if _flag.key not in value:
continue
if _flag.visibility == "system":
value.pop(_flag.key, None)
continue
flag_value = value.get(_flag.key)
flag_type = get_args(_flag.__orig_bases__[0])[0]
if flag_value and not isinstance(flag_value, flag_type):
raise ValidationError(
_("Value for flag {flag_key} needs to be of type {type}.").format(
flag_key=_flag.key, type=flag_type.__name__
)
)
class FlagsJSONExtension(OpenApiSerializerFieldExtension):

View File

@@ -4,6 +4,7 @@ from functools import wraps
from typing import TYPE_CHECKING, Any, Literal
from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models import F, Func, JSONField, Value
from authentik.lib.utils.reflection import all_subclasses
@@ -13,7 +14,9 @@ if TYPE_CHECKING:
class Flag[T]:
default: T | None = None
visibility: Literal["none"] | Literal["public"] | Literal["authenticated"] = "none"
visibility: (
Literal["none"] | Literal["public"] | Literal["authenticated"] | Literal["system"]
) = "none"
description: str | None = None
def __init_subclass__(cls, key: str, **kwargs):
@@ -24,12 +27,15 @@ class Flag[T]:
return self.__key
@classmethod
def get(cls) -> T | None:
def get(cls, tenant: Tenant | None = None) -> T | None:
from authentik.tenants.utils import get_current_tenant
if not tenant:
tenant = get_current_tenant(["flags"])
flags = {}
try:
flags: dict[str, Any] = get_current_tenant(["flags"]).flags
flags: dict[str, Any] = tenant.flags
except DatabaseError, ProgrammingError, InternalError:
pass
value = flags.get(cls.__key, None)
@@ -37,20 +43,38 @@ class Flag[T]:
return cls().get_default()
return value
@classmethod
def set(cls, value: T, tenant: Tenant | None = None) -> T | None:
from authentik.tenants.models import Tenant
from authentik.tenants.utils import get_current_tenant
if not tenant:
tenant = get_current_tenant()
Tenant.objects.filter(pk=tenant.pk).update(
flags=Func(
F("flags"),
Value([cls.__key]),
Value(value, JSONField()),
function="jsonb_set",
)
)
def get_default(self) -> T | None:
return self.default
@staticmethod
def available(
visibility: Literal["none"] | Literal["public"] | Literal["authenticated"] | None = None,
exclude_system=True,
):
flags = all_subclasses(Flag)
if visibility:
for flag in flags:
if flag.visibility == visibility:
yield flag
else:
yield from flags
for flag in flags:
if visibility and flag.visibility != visibility:
continue
if exclude_system and flag.visibility == "system":
continue
yield flag
def patch_flag[T](flag: Flag[T], value: T):

View File

@@ -0,0 +1,19 @@
from argparse import ArgumentParser
from typing import Any
from authentik.tenants.management import TenantCommand
from authentik.tenants.utils import get_current_tenant
class Command(TenantCommand):
def add_arguments(self, parser: ArgumentParser):
parser.add_argument("flag_key", type=str)
parser.add_argument("flag_value", type=str)
def handle(self, *, flag_key: str, flag_value: Any, **options):
tenant = get_current_tenant()
val = flag_value.lower() == "true"
tenant.flags[flag_key] = val
tenant.save()
self.stdout.write(f"Set flag '{flag_key}' to {val}.")

View File

@@ -1,10 +1,12 @@
"""Test Settings API"""
from django.core.management import call_command
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.tenants.flags import Flag
from authentik.tenants.utils import get_current_tenant
class TestLocalSettingsAPI(APITestCase):
@@ -13,11 +15,19 @@ class TestLocalSettingsAPI(APITestCase):
def setUp(self):
super().setUp()
self.local_admin = create_test_admin_user()
self.tenant = get_current_tenant()
def tearDown(self):
super().tearDown()
self.tenant.flags = {}
self.tenant.save()
def test_settings_flags(self):
"""Test settings API"""
self.tenant.flags = {}
self.tenant.save()
class TestFlag(Flag[bool], key="tenants_test_flag"):
class _TestFlag(Flag[bool], key="tenants_test_flag_bool"):
default = False
visibility = "public"
@@ -26,15 +36,19 @@ class TestLocalSettingsAPI(APITestCase):
response = self.client.patch(
reverse("authentik_api:tenant_settings"),
data={
"flags": {"tenants_test_flag": True},
"flags": {"tenants_test_flag_bool": True},
},
)
self.assertEqual(response.status_code, 200)
self.tenant.refresh_from_db()
self.assertEqual(self.tenant.flags["tenants_test_flag_bool"], True)
def test_settings_flags_incorrect(self):
"""Test settings API"""
self.tenant.flags = {}
self.tenant.save()
class TestFlag(Flag[bool], key="tenants_test_flag"):
class _TestFlag(Flag[bool], key="tenants_test_flag_incorrect"):
default = False
visibility = "public"
@@ -43,11 +57,44 @@ class TestLocalSettingsAPI(APITestCase):
response = self.client.patch(
reverse("authentik_api:tenant_settings"),
data={
"flags": {"tenants_test_flag": 123},
"flags": {"tenants_test_flag_incorrect": 123},
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"flags": ["Value for flag tenants_test_flag needs to be of type bool."]},
{"flags": ["Value for flag tenants_test_flag_incorrect needs to be of type bool."]},
)
self.tenant.refresh_from_db()
self.assertEqual(self.tenant.flags, {})
def test_settings_flags_system(self):
"""Test settings API"""
self.tenant.flags = {}
self.tenant.save()
class _TestFlag(Flag[bool], key="tenants_test_flag_sys"):
default = False
visibility = "system"
self.client.force_login(self.local_admin)
response = self.client.patch(
reverse("authentik_api:tenant_settings"),
data={
"flags": {"tenants_test_flag_sys": 123},
},
)
print(response.content)
self.assertEqual(response.status_code, 200)
self.tenant.refresh_from_db()
self.assertEqual(self.tenant.flags, {})
def test_command(self):
self.tenant.flags = {}
self.tenant.save()
call_command("set_flag", "foo", "true")
self.tenant.refresh_from_db()
self.assertTrue(self.tenant.flags["foo"])

View File

@@ -1,4 +1,7 @@
metadata:
labels:
blueprints.goauthentik.io/system-oobe: "true"
blueprints.goauthentik.io/system: "true"
name: Default - Out-of-box-experience flow
version: 1
entries:
@@ -75,23 +78,20 @@ entries:
- attrs:
expression: |
# This policy ensures that the setup flow can only be
# executed when the admin user doesn''t have a password set
# executed when the admin user doesn't have a password set
akadmin = ak_user_by(username="akadmin")
return not akadmin.has_usable_password()
# Ensure flow was started correctly
started_by = context.get("goauthentik.io/core/setup/started-by")
if started_by != "setup":
setup_url = request.http_request.build_absolute_uri("/")
ak_message(f"Access the authentik setup by navigating to {setup_url}")
return False
return akadmin is None or not akadmin.has_usable_password()
id: policy-default-oobe-password-usable
identifiers:
name: default-oobe-password-usable
model: authentik_policies_expression.expressionpolicy
- attrs:
expression: |
# This policy ensures that the setup flow can only be
# used one time
from authentik.flows.models import Flow, FlowAuthenticationRequirement
Flow.objects.filter(slug="initial-setup").update(
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
)
return True
id: policy-default-oobe-flow-set-authentication
- state: absent
identifiers:
name: default-oobe-flow-set-authentication
model: authentik_policies_expression.expressionpolicy
@@ -154,8 +154,3 @@ entries:
policy: !KeyOf policy-default-oobe-prefill-user
target: !KeyOf binding-password-write
model: authentik_policies.policybinding
- identifiers:
order: 0
policy: !KeyOf policy-default-oobe-flow-set-authentication
target: !KeyOf binding-login
model: authentik_policies.policybinding

View File

@@ -84,12 +84,6 @@ if [[ "$1" == "server" ]]; then
elif [[ "$1" == "worker" ]]; then
set_mode "worker"
shift
# If we have bootstrap credentials set, run bootstrap tasks outside of main server
# sync, so that we can sure the first start actually has working bootstrap
# credentials
if [[ -n "${AUTHENTIK_BOOTSTRAP_PASSWORD}" || -n "${AUTHENTIK_BOOTSTRAP_TOKEN}" ]]; then
python -m manage apply_blueprint system/bootstrap.yaml || true
fi
check_if_root "python -m manage worker --pid-file ${TMPDIR}/authentik-worker.pid $@"
elif [[ "$1" == "bash" ]]; then
/bin/bash

View File

@@ -6,6 +6,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from dramatiq import get_broker
from structlog.stdlib import get_logger
from authentik.core.apps import Setup
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.tasks.test import use_test_broker
@@ -27,6 +28,7 @@ class E2ETestMixin(DockerTestCase):
self.wait_timeout = 60
self.logger = get_logger()
self.user = create_test_admin_user()
Setup.set(True)
super().setUp()
@classmethod

View File

@@ -62,5 +62,3 @@ If you're focusing solely on frontend development, you can create a minimal deve
```
You can now access authentik on http://localhost:9000 (or https://localhost:9443).
You might also want to complete the initial setup under `/if/flow/initial-setup/`.

View File

@@ -174,7 +174,7 @@ make web-watch
After the frontend build completes, set a password for the default admin user (**akadmin**):
1. Navigate to http://localhost:9000/if/flow/initial-setup/ in your browser.
1. Navigate to http://localhost:9000 in your browser.
2. Follow the prompts to set up your admin account.
From now on, you can access authentik at http://localhost:9000 using the credentials you defined in Step 2.

View File

@@ -30,12 +30,12 @@ The stack will output the endpoint of the ALB to which you can point your DNS re
## Access authentik from AWS CloudFormation
To launch authentik, in your browser go to:
To start the initial setup, navigate to `http://<domain_you_configured>`.
`http://<domain_you_configured>/if/flow/initial-setup/`
You are then prompted to set a password for the `akadmin` user (the default user).
:::info Initial setup in browser
You will get a `Not Found` error if initial setup URL doesn't include the trailing forward slash `/`. Also verify that the authentik server, worker, and PostgreSQL database are running and healthy. Review additional tips in our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
:::info Issues with initial setup
If you run into issues, refer to our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
:::
### Further customization

View File

@@ -116,14 +116,14 @@ The `compose.yml` file statically references the latest version available at the
## Access authentik
To start the initial setup, navigate to `http://<your server's IP or hostname>:9000/if/flow/initial-setup/`.
:::info Initial setup in browser
You will get a `Not Found` error if initial setup URL doesn't include the trailing forward slash `/`. Also verify that the authentik server, worker, and PostgreSQL database are running and healthy. Review additional tips in our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
:::
To start the initial setup, navigate to `http://<your server's IP or hostname>:9000`.
You are then prompted to set a password for the `akadmin` user (the default user).
:::info Issues with initial setup
If you run into issues, refer to our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
:::
## First steps in authentik
import BlurbFirstSteps from "../first-steps/_blurb_first_steps.mdx";

View File

@@ -109,10 +109,12 @@ During the installation process, the database migrations will be applied automat
## Access authentik
After the installation is complete, access authentik at `https://<authentik-host-name>/if/flow/initial-setup/`. Here, you can set a password for the default `akadmin` user.
To start the initial setup, navigate to `http://<your server's IP or hostname>:9000`.
:::info Initial setup in browser
You will get a `Not Found` error if initial setup URL doesn't include the trailing forward slash `/`. Also verify that the authentik server, worker, and PostgreSQL database are running and healthy. Review additional tips in our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
You are then prompted to set a password for the `akadmin` user (the default user).
:::info Issues with initial setup
If you run into issues, refer to our [troubleshooting docs](../../troubleshooting/login.md#cant-access-initial-setup-flow-during-installation-steps).
:::
## First steps in authentik