diff --git a/authentik/admin/views/certificate_key_pair.py b/authentik/admin/views/certificate_key_pair.py
index 9a093cfdc..a08d334b2 100644
--- a/authentik/admin/views/certificate_key_pair.py
+++ b/authentik/admin/views/certificate_key_pair.py
@@ -33,7 +33,7 @@ class CertificateKeyPairCreateView(
permission_required = "authentik_crypto.add_certificatekeypair"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Certificate-Key Pair")
@@ -50,7 +50,7 @@ class CertificateKeyPairGenerateView(
permission_required = "authentik_crypto.add_certificatekeypair"
template_name = "administration/certificatekeypair/generate.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully generated Certificate-Key Pair")
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
@@ -77,5 +77,5 @@ class CertificateKeyPairUpdateView(
permission_required = "authentik_crypto.change_certificatekeypair"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Certificate-Key Pair")
diff --git a/authentik/admin/views/flows.py b/authentik/admin/views/flows.py
index 936497fdd..01d1b54fa 100644
--- a/authentik/admin/views/flows.py
+++ b/authentik/admin/views/flows.py
@@ -34,7 +34,7 @@ class FlowCreateView(
permission_required = "authentik_flows.add_flow"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Flow")
@@ -51,7 +51,7 @@ class FlowUpdateView(
permission_required = "authentik_flows.change_flow"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Flow")
@@ -79,7 +79,7 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
),
)
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
@@ -91,7 +91,7 @@ class FlowImportView(LoginRequiredMixin, FormView):
form_class = FlowImportForm
template_name = "administration/flow/import.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
diff --git a/authentik/admin/views/groups.py b/authentik/admin/views/groups.py
index d50321e44..302280259 100644
--- a/authentik/admin/views/groups.py
+++ b/authentik/admin/views/groups.py
@@ -27,7 +27,7 @@ class GroupCreateView(
permission_required = "authentik_core.add_group"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Group")
@@ -44,5 +44,5 @@ class GroupUpdateView(
permission_required = "authentik_core.change_group"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Group")
diff --git a/authentik/admin/views/outposts_service_connections.py b/authentik/admin/views/outposts_service_connections.py
index a69403a1e..9ffc5db9e 100644
--- a/authentik/admin/views/outposts_service_connections.py
+++ b/authentik/admin/views/outposts_service_connections.py
@@ -24,7 +24,7 @@ class OutpostServiceConnectionCreateView(
permission_required = "authentik_outposts.add_outpostserviceconnection"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Outpost Service Connection")
@@ -40,5 +40,5 @@ class OutpostServiceConnectionUpdateView(
permission_required = "authentik_outposts.change_outpostserviceconnection"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Outpost Service Connection")
diff --git a/authentik/admin/views/policies.py b/authentik/admin/views/policies.py
index 027ef2e5b..93ea9f50e 100644
--- a/authentik/admin/views/policies.py
+++ b/authentik/admin/views/policies.py
@@ -31,7 +31,7 @@ class PolicyCreateView(
permission_required = "authentik_policies.add_policy"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Policy")
@@ -47,7 +47,7 @@ class PolicyUpdateView(
permission_required = "authentik_policies.change_policy"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Policy")
diff --git a/authentik/admin/views/policies_bindings.py b/authentik/admin/views/policies_bindings.py
index 33b96008c..fbbd33f29 100644
--- a/authentik/admin/views/policies_bindings.py
+++ b/authentik/admin/views/policies_bindings.py
@@ -30,7 +30,7 @@ class PolicyBindingCreateView(
form_class = PolicyBindingForm
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created PolicyBinding")
def get_initial(self) -> dict[str, Any]:
@@ -63,5 +63,5 @@ class PolicyBindingUpdateView(
form_class = PolicyBindingForm
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated PolicyBinding")
diff --git a/authentik/admin/views/stages.py b/authentik/admin/views/stages.py
index 603a2b612..39ff3ffbb 100644
--- a/authentik/admin/views/stages.py
+++ b/authentik/admin/views/stages.py
@@ -24,7 +24,7 @@ class StageCreateView(
template_name = "generic/create.html"
permission_required = "authentik_flows.add_stage"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Stage")
@@ -39,5 +39,5 @@ class StageUpdateView(
model = Stage
permission_required = "authentik_flows.update_application"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Stage")
diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py
index 5621bf01e..3bebcb75c 100644
--- a/authentik/admin/views/stages_bindings.py
+++ b/authentik/admin/views/stages_bindings.py
@@ -30,7 +30,7 @@ class StageBindingCreateView(
form_class = FlowStageBindingForm
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created StageBinding")
def get_initial(self) -> dict[str, Any]:
@@ -61,5 +61,5 @@ class StageBindingUpdateView(
form_class = FlowStageBindingForm
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated StageBinding")
diff --git a/authentik/admin/views/stages_invitations.py b/authentik/admin/views/stages_invitations.py
index afbcfee13..ca2552c58 100644
--- a/authentik/admin/views/stages_invitations.py
+++ b/authentik/admin/views/stages_invitations.py
@@ -26,7 +26,7 @@ class InvitationCreateView(
permission_required = "authentik_stages_invitation.add_invitation"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Invitation")
def form_valid(self, form):
diff --git a/authentik/admin/views/stages_prompts.py b/authentik/admin/views/stages_prompts.py
index 8ef5feb1f..f47e6dba1 100644
--- a/authentik/admin/views/stages_prompts.py
+++ b/authentik/admin/views/stages_prompts.py
@@ -27,7 +27,7 @@ class PromptCreateView(
permission_required = "authentik_stages_prompt.add_prompt"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Prompt")
@@ -44,5 +44,5 @@ class PromptUpdateView(
permission_required = "authentik_stages_prompt.change_prompt"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Prompt")
diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py
index 7d6f60e78..760c67725 100644
--- a/authentik/admin/views/users.py
+++ b/authentik/admin/views/users.py
@@ -31,7 +31,7 @@ class UserCreateView(
permission_required = "authentik_core.add_user"
template_name = "generic/create.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created User")
@@ -50,7 +50,7 @@ class UserUpdateView(
# By default the object's name is user which is used by other checks
context_object_name = "object"
template_name = "generic/update.html"
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated User")
diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py
index 28fa1a08c..c7ebe9eaf 100644
--- a/authentik/admin/views/utils.py
+++ b/authentik/admin/views/utils.py
@@ -14,7 +14,7 @@ from authentik.lib.views import CreateAssignPermView
class DeleteMessageView(SuccessMessageMixin, DeleteView):
"""DeleteView which shows `self.success_message` on successful deletion"""
- success_url = reverse_lazy("authentik_core:shell")
+ success_url = reverse_lazy("authentik_core:if-admin")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
diff --git a/authentik/api/auth.py b/authentik/api/auth.py
index dc51dcaae..52f59ab5b 100644
--- a/authentik/api/auth.py
+++ b/authentik/api/auth.py
@@ -10,6 +10,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Token, TokenIntents, User
LOGGER = get_logger()
+X_AUTHENTIK_PREVENT_BASIC_HEADER = "HTTP_X_AUTHENTIK_PREVENT_BASIC"
def token_from_header(raw_header: bytes) -> Optional[Token]:
@@ -55,4 +56,6 @@ class AuthentikTokenAuthentication(BaseAuthentication):
return (token.user, None)
def authenticate_header(self, request: Request) -> str:
+ if X_AUTHENTIK_PREVENT_BASIC_HEADER in request._request.META:
+ return ""
return 'Basic realm="authentik"'
diff --git a/authentik/api/urls.py b/authentik/api/urls.py
index b4c7791b9..d3eadf03c 100644
--- a/authentik/api/urls.py
+++ b/authentik/api/urls.py
@@ -1,8 +1,10 @@
"""authentik api urls"""
from django.urls import include, path
+from django.views.i18n import JavaScriptCatalog
from authentik.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [
path("v2beta/", include(v2_urls)),
+ path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
]
diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py
index 6c95a0983..c8c04082c 100644
--- a/authentik/core/api/users.py
+++ b/authentik/core/api/users.py
@@ -10,6 +10,10 @@ from rest_framework.serializers import BooleanField, ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
+from authentik.core.middleware import (
+ SESSION_IMPERSONATE_ORIGINAL_USER,
+ SESSION_IMPERSONATE_USER,
+)
from authentik.core.models import User
from authentik.events.models import EventAction
@@ -36,6 +40,20 @@ class UserSerializer(ModelSerializer):
]
+class SessionUserSerializer(Serializer):
+ """Response for the /user/me endpoint, returns the currently active user (as `user` property)
+ and, if this user is being impersonated, the original user in the `original` property."""
+
+ user = UserSerializer()
+ original = UserSerializer(required=False)
+
+ def create(self, validated_data: dict) -> Model:
+ raise NotImplementedError
+
+ def update(self, instance: Model, validated_data: dict) -> Model:
+ raise NotImplementedError
+
+
class UserMetricsSerializer(Serializer):
"""User Metrics"""
@@ -83,12 +101,20 @@ class UserViewSet(ModelViewSet):
def get_queryset(self):
return User.objects.all().exclude(pk=get_anonymous_user().pk)
- @swagger_auto_schema(responses={200: UserSerializer(many=False)})
+ @swagger_auto_schema(responses={200: SessionUserSerializer(many=False)})
@action(detail=False)
# pylint: disable=invalid-name
def me(self, request: Request) -> Response:
"""Get information about current user"""
- return Response(UserSerializer(request.user).data)
+ serializer = SessionUserSerializer(
+ data={"user": UserSerializer(request.user).data}
+ )
+ if SESSION_IMPERSONATE_USER in request._request.session:
+ serializer.initial_data["original"] = UserSerializer(
+ request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
+ ).data
+ serializer.is_valid()
+ return Response(serializer.data)
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=False)
diff --git a/authentik/core/templates/base/skeleton.html b/authentik/core/templates/base/skeleton.html
index e22bd2565..7e8eb1772 100644
--- a/authentik/core/templates/base/skeleton.html
+++ b/authentik/core/templates/base/skeleton.html
@@ -13,21 +13,11 @@
-
+
{% block head %}
{% endblock %}
- {% if 'authentik_impersonate_user' in request.session %}
-
- {% endif %}
{% block body %}
{% endblock %}
{% block scripts %}
diff --git a/authentik/core/tests/test_impersonation.py b/authentik/core/tests/test_impersonation.py
index cce520c17..9a30ba16c 100644
--- a/authentik/core/tests/test_impersonation.py
+++ b/authentik/core/tests/test_impersonation.py
@@ -1,4 +1,6 @@
"""impersonation tests"""
+from json import loads
+
from django.test.testcases import TestCase
from django.urls import reverse
@@ -25,14 +27,16 @@ class TestImpersonation(TestCase):
)
response = self.client.get(reverse("authentik_api:user-me"))
- self.assertIn(self.other_user.username, response.content.decode())
- self.assertNotIn(self.akadmin.username, response.content.decode())
+ response_body = loads(response.content.decode())
+ self.assertEqual(response_body["user"]["username"], self.other_user.username)
+ self.assertEqual(response_body["original"]["username"], self.akadmin.username)
self.client.get(reverse("authentik_core:impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me"))
- self.assertNotIn(self.other_user.username, response.content.decode())
- self.assertIn(self.akadmin.username, response.content.decode())
+ response_body = loads(response.content.decode())
+ self.assertEqual(response_body["user"]["username"], self.akadmin.username)
+ self.assertNotIn("original", response_body)
def test_impersonate_denied(self):
"""test impersonation without permissions"""
@@ -45,12 +49,12 @@ class TestImpersonation(TestCase):
)
response = self.client.get(reverse("authentik_api:user-me"))
- self.assertIn(self.other_user.username, response.content.decode())
- self.assertNotIn(self.akadmin.username, response.content.decode())
+ response_body = loads(response.content.decode())
+ self.assertEqual(response_body["user"]["username"], self.other_user.username)
def test_un_impersonate_empty(self):
"""test un-impersonation without impersonating first"""
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end"))
- self.assertRedirects(response, reverse("authentik_core:shell"))
+ self.assertRedirects(response, reverse("authentik_core:if-admin"))
diff --git a/authentik/core/tests/test_views_overview.py b/authentik/core/tests/test_views_overview.py
deleted file mode 100644
index e6eafdc75..000000000
--- a/authentik/core/tests/test_views_overview.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""authentik user view tests"""
-import string
-from random import SystemRandom
-
-from django.test import TestCase
-from django.urls import reverse
-
-from authentik.core.models import User
-
-
-class TestOverviewViews(TestCase):
- """Test Overview Views"""
-
- def setUp(self):
- super().setUp()
- self.user = User.objects.create_user(
- username="unittest user",
- email="unittest@example.com",
- password="".join(
- SystemRandom().choice(string.ascii_uppercase + string.digits)
- for _ in range(8)
- ),
- )
- self.client.force_login(self.user)
-
- def test_shell(self):
- """Test shell"""
- self.assertEqual(
- self.client.get(reverse("authentik_core:shell")).status_code, 200
- )
diff --git a/authentik/core/urls.py b/authentik/core/urls.py
index bdf339319..d104f4c32 100644
--- a/authentik/core/urls.py
+++ b/authentik/core/urls.py
@@ -1,10 +1,19 @@
"""authentik URL Configuration"""
+from django.contrib.auth.decorators import login_required
from django.urls import path
+from django.views.decorators.csrf import ensure_csrf_cookie
+from django.views.generic import RedirectView
+from django.views.generic.base import TemplateView
-from authentik.core.views import impersonate, shell, user
+from authentik.core.views import impersonate, user
+from authentik.flows.views import FlowExecutorShellView
urlpatterns = [
- path("", shell.ShellView.as_view(), name="shell"),
+ path(
+ "",
+ login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")),
+ name="root-redirect",
+ ),
# User views
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
path(
@@ -28,4 +37,15 @@ urlpatterns = [
impersonate.ImpersonateEndView.as_view(),
name="impersonate-end",
),
+ # Interfaces
+ path(
+ "if/admin/",
+ ensure_csrf_cookie(TemplateView.as_view(template_name="shell.html")),
+ name="if-admin",
+ ),
+ path(
+ "if/flow//",
+ ensure_csrf_cookie(FlowExecutorShellView.as_view()),
+ name="if-flow",
+ ),
]
diff --git a/authentik/core/views/impersonate.py b/authentik/core/views/impersonate.py
index 1d93a02a6..4b5523d44 100644
--- a/authentik/core/views/impersonate.py
+++ b/authentik/core/views/impersonate.py
@@ -33,7 +33,7 @@ class ImpersonateInitView(View):
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
- return redirect("authentik_core:shell")
+ return redirect("authentik_core:if-admin")
class ImpersonateEndView(View):
@@ -46,7 +46,7 @@ class ImpersonateEndView(View):
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
- return redirect("authentik_core:shell")
+ return redirect("authentik_core:if-admin")
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
@@ -55,4 +55,4 @@ class ImpersonateEndView(View):
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
- return redirect("authentik_core:shell")
+ return redirect("authentik_core:root-redirect")
diff --git a/authentik/core/views/shell.py b/authentik/core/views/shell.py
deleted file mode 100644
index 9aa280e08..000000000
--- a/authentik/core/views/shell.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""core shell view"""
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.views.generic.base import TemplateView
-
-
-class ShellView(LoginRequiredMixin, TemplateView):
- """core shell view"""
-
- template_name = "shell.html"
diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py
index 0abf925d3..2a7b07e78 100644
--- a/authentik/flows/challenge.py
+++ b/authentik/flows/challenge.py
@@ -43,6 +43,7 @@ class Challenge(Serializer):
)
component = CharField(required=False)
title = CharField(required=False)
+ background = CharField(required=False)
response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=False, required=False
diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py
index 59265d8aa..0f59e9c56 100644
--- a/authentik/flows/stage.py
+++ b/authentik/flows/stage.py
@@ -79,6 +79,8 @@ class ChallengeStageView(StageView):
challenge = self.get_challenge(*args, **kwargs)
if "title" not in challenge.initial_data:
challenge.initial_data["title"] = self.executor.flow.title
+ if "background" not in challenge.initial_data:
+ challenge.initial_data["background"] = self.executor.flow.background.url
if isinstance(challenge, WithUserInfoChallenge):
# If there's a pending user, update the `username` field
# this field is only used by password managers.
diff --git a/authentik/flows/tests/test_views.py b/authentik/flows/tests/test_views.py
index 0c59dbf77..196409ec5 100644
--- a/authentik/flows/tests/test_views.py
+++ b/authentik/flows/tests/test_views.py
@@ -109,7 +109,7 @@ class TestFlowExecutor(TestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertEqual(response.status_code, 302)
- self.assertEqual(response.url, reverse("authentik_core:shell"))
+ self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
@patch(
"authentik.flows.views.to_stage_response",
@@ -128,7 +128,7 @@ class TestFlowExecutor(TestCase):
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
self.assertEqual(response.status_code, 302)
- self.assertEqual(response.url, reverse("authentik_core:shell"))
+ self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def test_multi_stage_flow(self):
"""Test a full flow with multiple stages"""
@@ -217,7 +217,7 @@ class TestFlowExecutor(TestCase):
# We do this request without the patch, so the policy results in false
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
- self.assertEqual(response.url, reverse("authentik_core:shell"))
+ self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def test_reevaluate_remove_middle(self):
"""Test planner with re-evaluate (middle stage is removed)"""
@@ -283,7 +283,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_reevaluate_keep(self):
@@ -360,7 +360,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_reevaluate_remove_consecutive(self):
@@ -408,6 +408,7 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
+ "background": flow.background.url,
"type": ChallengeTypes.native.value,
"component": "",
"title": binding.stage.name,
@@ -438,6 +439,7 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
+ "background": flow.background.url,
"type": ChallengeTypes.native.value,
"component": "",
"title": binding4.stage.name,
@@ -450,7 +452,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_stageview_user_identifier(self):
diff --git a/authentik/flows/tests/test_views_helper.py b/authentik/flows/tests/test_views_helper.py
index c42fcdc66..23bb5d5c5 100644
--- a/authentik/flows/tests/test_views_helper.py
+++ b/authentik/flows/tests/test_views_helper.py
@@ -22,7 +22,7 @@ class TestHelperView(TestCase):
reverse("authentik_flows:default-invalidation"),
)
expected_url = reverse(
- "authentik_flows:flow-executor-shell", kwargs={"flow_slug": flow.slug}
+ "authentik_core:if-flow", kwargs={"flow_slug": flow.slug}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
@@ -41,7 +41,7 @@ class TestHelperView(TestCase):
reverse("authentik_flows:default-invalidation"),
)
expected_url = reverse(
- "authentik_flows:flow-executor-shell", kwargs={"flow_slug": flow.slug}
+ "authentik_core:if-flow", kwargs={"flow_slug": flow.slug}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
diff --git a/authentik/flows/urls.py b/authentik/flows/urls.py
index 2c94ff3f4..12bbbfc0d 100644
--- a/authentik/flows/urls.py
+++ b/authentik/flows/urls.py
@@ -1,14 +1,9 @@
"""flow urls"""
from django.urls import path
-from django.views.decorators.csrf import ensure_csrf_cookie
+from django.views.generic import RedirectView
from authentik.flows.models import FlowDesignation
-from authentik.flows.views import (
- CancelView,
- ConfigureFlowInitView,
- FlowExecutorShellView,
- ToDefaultFlow,
-)
+from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow
urlpatterns = [
path(
@@ -44,7 +39,7 @@ urlpatterns = [
),
path(
"/",
- ensure_csrf_cookie(FlowExecutorShellView.as_view()),
+ RedirectView.as_view(pattern_name="authentik_core:if-flow"),
name="flow-executor-shell",
),
]
diff --git a/authentik/flows/views.py b/authentik/flows/views.py
index c44d1708f..d45c814a9 100644
--- a/authentik/flows/views.py
+++ b/authentik/flows/views.py
@@ -173,7 +173,7 @@ class FlowExecutorView(APIView):
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
if not next_param:
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
- NEXT_ARG_NAME, "authentik_core:shell"
+ NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
return to_stage_response(self.request, redirect_with_qs(next_param))
@@ -263,11 +263,8 @@ class FlowExecutorShellView(TemplateView):
template_name = "flows/shell.html"
def get_context_data(self, **kwargs) -> dict[str, Any]:
- flow: Flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
- kwargs["background_url"] = flow.background.url
- kwargs["flow_slug"] = flow.slug
self.request.session[SESSION_KEY_GET] = self.request.GET
- return kwargs
+ return super().get_context_data(**kwargs)
class CancelView(View):
@@ -278,7 +275,7 @@ class CancelView(View):
if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN]
LOGGER.debug("Canceled current plan")
- return redirect("authentik_core:shell")
+ return redirect("authentik_core:root-redirect")
class ToDefaultFlow(View):
@@ -300,7 +297,7 @@ class ToDefaultFlow(View):
)
del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs(
- "authentik_flows:flow-executor-shell", request.GET, flow_slug=flow.slug
+ "authentik_core:if-flow", request.GET, flow_slug=flow.slug
)
@@ -360,7 +357,7 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
)
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
self.request.GET,
flow_slug=stage.configure_flow.slug,
)
diff --git a/authentik/providers/oauth2/templates/providers/oauth2/end_session.html b/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
index 55ed8988a..a71cd59c4 100644
--- a/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
+++ b/authentik/providers/oauth2/templates/providers/oauth2/end_session.html
@@ -32,7 +32,7 @@ You've logged out of {{ application }}.
{% endblocktrans %}
- {% trans 'Go back to overview' %}
+ {% trans 'Go back to overview' %}
{% trans 'Log out of authentik' %}
diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py
index fa8649917..9962a37bb 100644
--- a/authentik/providers/oauth2/views/authorize.py
+++ b/authentik/providers/oauth2/views/authorize.py
@@ -464,7 +464,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
plan.append(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
self.request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
diff --git a/authentik/providers/saml/views/sso.py b/authentik/providers/saml/views/sso.py
index ae869ca47..4eb76af68 100644
--- a/authentik/providers/saml/views/sso.py
+++ b/authentik/providers/saml/views/sso.py
@@ -82,7 +82,7 @@ class SAMLSSOView(PolicyAccessView):
plan.append(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
diff --git a/authentik/recovery/views.py b/authentik/recovery/views.py
index b0c1978ce..76cb9636a 100644
--- a/authentik/recovery/views.py
+++ b/authentik/recovery/views.py
@@ -21,4 +21,4 @@ class UseTokenView(View):
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
token.delete()
messages.warning(request, _("Used recovery-link to authenticate."))
- return redirect("authentik_core:shell")
+ return redirect("authentik_core:if-admin")
diff --git a/authentik/root/urls.py b/authentik/root/urls.py
index 5b9d77ddb..e892bc358 100644
--- a/authentik/root/urls.py
+++ b/authentik/root/urls.py
@@ -4,7 +4,6 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
-from django.views.i18n import JavaScriptCatalog
from structlog.stdlib import get_logger
from authentik.core.views import error
@@ -59,7 +58,6 @@ urlpatterns += [
path("metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
- path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
]
if settings.DEBUG:
diff --git a/authentik/sources/oauth/views/callback.py b/authentik/sources/oauth/views/callback.py
index 54a294826..f121ef338 100644
--- a/authentik/sources/oauth/views/callback.py
+++ b/authentik/sources/oauth/views/callback.py
@@ -141,7 +141,7 @@ class OAuthCallback(OAuthClientMixin, View):
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
- NEXT_ARG_NAME, "authentik_core:shell"
+ NEXT_ARG_NAME, "authentik_core:if-admin"
)
kwargs.update(
{
@@ -159,7 +159,7 @@ class OAuthCallback(OAuthClientMixin, View):
plan = planner.plan(self.request, kwargs)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
@@ -244,7 +244,7 @@ class OAuthCallback(OAuthClientMixin, View):
plan.append(in_memory_stage(PostUserEnrollmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
self.request.GET,
flow_slug=source.enrollment_flow.slug,
)
diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py
index 00eff2f3d..74793ba8d 100644
--- a/authentik/sources/saml/processors/response.py
+++ b/authentik/sources/saml/processors/response.py
@@ -195,7 +195,7 @@ class ResponseProcessor:
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get(
- NEXT_ARG_NAME, "authentik_core:shell"
+ NEXT_ARG_NAME, "authentik_core:if-admin"
)
if matching_users.exists():
# User exists already, switch to authentication flow
@@ -221,7 +221,7 @@ class ResponseProcessor:
kwargs[PLAN_CONTEXT_SOURCE] = self._source
request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs)
return redirect_with_qs(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
request.GET,
flow_slug=flow.slug,
)
diff --git a/authentik/stages/captcha/tests.py b/authentik/stages/captcha/tests.py
index aaf025cac..b6b5f5bae 100644
--- a/authentik/stages/captcha/tests.py
+++ b/authentik/stages/captcha/tests.py
@@ -54,5 +54,5 @@ class TestCaptchaStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
diff --git a/authentik/stages/consent/tests.py b/authentik/stages/consent/tests.py
index cc75031bb..bdd8bc0a9 100644
--- a/authentik/stages/consent/tests.py
+++ b/authentik/stages/consent/tests.py
@@ -51,7 +51,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
self.assertFalse(UserConsent.objects.filter(user=self.user).exists())
@@ -82,7 +82,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
self.assertTrue(
UserConsent.objects.filter(
@@ -119,7 +119,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
self.assertTrue(
UserConsent.objects.filter(
diff --git a/authentik/stages/dummy/tests.py b/authentik/stages/dummy/tests.py
index bdbdb9e33..02dd11876 100644
--- a/authentik/stages/dummy/tests.py
+++ b/authentik/stages/dummy/tests.py
@@ -47,7 +47,7 @@ class TestDummyStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_form(self):
diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py
index de3b2d394..1c24c5080 100644
--- a/authentik/stages/email/stage.py
+++ b/authentik/stages/email/stage.py
@@ -45,7 +45,7 @@ class EmailStageView(ChallengeStageView):
def get_full_url(self, **kwargs) -> str:
"""Get full URL to be used in template"""
base_url = reverse(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
kwargs={"flow_slug": self.executor.flow.slug},
)
relative_url = f"{base_url}?{urlencode(kwargs)}"
diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py
index 94e1d3c45..aeba8c385 100644
--- a/authentik/stages/email/tests/test_stage.py
+++ b/authentik/stages/email/tests/test_stage.py
@@ -109,7 +109,7 @@ class TestEmailStage(TestCase):
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
url = reverse(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
kwargs={"flow_slug": self.flow.slug},
)
token = Token.objects.get(user=self.user)
@@ -126,7 +126,7 @@ class TestEmailStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
session = self.client.session
diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py
index a9365f515..4338af217 100644
--- a/authentik/stages/identification/stage.py
+++ b/authentik/stages/identification/stage.py
@@ -95,12 +95,12 @@ class IdentificationStageView(ChallengeStageView):
# Check for related enrollment and recovery flow, add URL to view
if current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
)
if current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
kwargs={"flow_slug": current_stage.recovery_flow.slug},
)
diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py
index ccda61683..1e0daebb6 100644
--- a/authentik/stages/identification/tests.py
+++ b/authentik/stages/identification/tests.py
@@ -53,7 +53,7 @@ class TestIdentificationStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_invalid_with_username(self):
@@ -103,10 +103,14 @@ class TestIdentificationStage(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
+ "background": flow.background.url,
"type": ChallengeTypes.native.value,
"component": "ak-stage-identification",
"input_type": "email",
- "enroll_url": "/flows/unique-enrollment-string/",
+ "enroll_url": reverse(
+ "authentik_core:if-flow",
+ kwargs={"flow_slug": "unique-enrollment-string"},
+ ),
"primary_action": "Log in",
"title": self.flow.title,
"sources": [
@@ -142,10 +146,14 @@ class TestIdentificationStage(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
+ "background": flow.background.url,
"type": ChallengeTypes.native.value,
"component": "ak-stage-identification",
"input_type": "email",
- "recovery_url": "/flows/unique-recovery-string/",
+ "recovery_url": reverse(
+ "authentik_core:if-flow",
+ kwargs={"flow_slug": "unique-recovery-string"},
+ ),
"primary_action": "Log in",
"title": self.flow.title,
"sources": [
diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py
index 156ae2529..0ae180c24 100644
--- a/authentik/stages/invitation/tests.py
+++ b/authentik/stages/invitation/tests.py
@@ -85,7 +85,7 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
self.stage.continue_flow_without_invitation = False
@@ -124,5 +124,5 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py
index 497c7d32c..d7ea70f41 100644
--- a/authentik/stages/password/stage.py
+++ b/authentik/stages/password/stage.py
@@ -85,7 +85,7 @@ class PasswordStageView(ChallengeStageView):
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists():
recover_url = reverse(
- "authentik_flows:flow-executor-shell",
+ "authentik_core:if-flow",
kwargs={"flow_slug": recovery_flow.first().slug},
)
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(
diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py
index 6c39e341b..2e5fb6933 100644
--- a/authentik/stages/password/tests.py
+++ b/authentik/stages/password/tests.py
@@ -110,7 +110,7 @@ class TestPasswordStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_invalid_password(self):
diff --git a/authentik/stages/prompt/tests.py b/authentik/stages/prompt/tests.py
index 58be327cd..941a99142 100644
--- a/authentik/stages/prompt/tests.py
+++ b/authentik/stages/prompt/tests.py
@@ -167,7 +167,7 @@ class TestPromptStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
# Check that valid data has been saved
diff --git a/authentik/stages/user_delete/tests.py b/authentik/stages/user_delete/tests.py
index 8a53b75c8..5062695aa 100644
--- a/authentik/stages/user_delete/tests.py
+++ b/authentik/stages/user_delete/tests.py
@@ -85,7 +85,7 @@ class TestUserDeleteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
self.assertFalse(User.objects.filter(username=self.username).exists())
diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py
index c84960a8f..05320d36c 100644
--- a/authentik/stages/user_login/tests.py
+++ b/authentik/stages/user_login/tests.py
@@ -53,7 +53,7 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
@patch(
diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py
index 7edd92ccc..4923dff93 100644
--- a/authentik/stages/user_logout/tests.py
+++ b/authentik/stages/user_logout/tests.py
@@ -49,7 +49,7 @@ class TestUserLogoutStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
def test_form(self):
diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py
index 0cf220d57..277dd2caa 100644
--- a/authentik/stages/user_write/tests.py
+++ b/authentik/stages/user_write/tests.py
@@ -61,7 +61,7 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
@@ -98,7 +98,7 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
- {"to": reverse("authentik_core:shell"), "type": "redirect"},
+ {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
)
user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
diff --git a/docker-compose.yml b/docker-compose.yml
index d915b32b6..839caa482 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -72,7 +72,7 @@ services:
labels:
traefik.enable: 'true'
traefik.docker.network: internal
- traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/media`, `/robots.txt`, `/favicon.ico`)
+ traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/if`, `/media`, `/robots.txt`, `/favicon.ico`)
traefik.http.routers.static-router.tls: 'true'
traefik.http.routers.static-router.service: static-service
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml
index 3d2ce18ac..262deb08c 100644
--- a/helm/templates/ingress.yaml
+++ b/helm/templates/ingress.yaml
@@ -36,6 +36,10 @@ spec:
backend:
serviceName: {{ $fullName }}-static
servicePort: http
+ - path: /if/
+ backend:
+ serviceName: {{ $fullName }}-static
+ servicePort: http
- path: /media/
backend:
serviceName: {{ $fullName }}-static
diff --git a/swagger.yaml b/swagger.yaml
index d94c14e3b..89b19bd8f 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -1611,9 +1611,11 @@ paths:
type: integer
responses:
'200':
- description: User Serializer
+ description: Response for the /user/me endpoint, returns the currently active
+ user (as `user` property) and, if this user is being impersonated, the
+ original user in the `original` property.
schema:
- $ref: '#/definitions/User'
+ $ref: '#/definitions/SessionUser'
tags:
- core
parameters: []
@@ -11029,6 +11031,18 @@ definitions:
$ref: '#/definitions/User'
application:
$ref: '#/definitions/Application'
+ SessionUser:
+ description: Response for the /user/me endpoint, returns the currently active
+ user (as `user` property) and, if this user is being impersonated, the original
+ user in the `original` property.
+ required:
+ - user
+ type: object
+ properties:
+ user:
+ $ref: '#/definitions/User'
+ original:
+ $ref: '#/definitions/User'
UserMetrics:
description: User Metrics
type: object
@@ -11533,6 +11547,10 @@ definitions:
title: Title
type: string
minLength: 1
+ background:
+ title: Background
+ type: string
+ minLength: 1
response_errors:
title: Response errors
type: object
@@ -13332,7 +13350,7 @@ definitions:
type: string
readOnly: true
Link:
- description: ''
+ description: Links returned in Config API
type: object
properties:
href:
diff --git a/tests/e2e/test_flows_authenticators.py b/tests/e2e/test_flows_authenticators.py
index 3784cbb56..a6c485c4a 100644
--- a/tests/e2e/test_flows_authenticators.py
+++ b/tests/e2e/test_flows_authenticators.py
@@ -39,7 +39,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
target=flow, order=30, stage=AuthenticatorValidateStage.objects.create()
)
- self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
+ self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
# Get expected token
@@ -59,7 +59,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(
Keys.ENTER
)
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER())
@retry()
@@ -70,10 +70,10 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
- self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
+ self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER())
self.driver.get(
@@ -120,10 +120,10 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
- self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
+ self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER())
self.driver.get(
diff --git a/tests/e2e/test_flows_enroll.py b/tests/e2e/test_flows_enroll.py
index f8ddbde61..6f5400293 100644
--- a/tests/e2e/test_flows_enroll.py
+++ b/tests/e2e/test_flows_enroll.py
@@ -97,7 +97,7 @@ class TestFlowsEnroll(SeleniumTestCase):
wait = WebDriverWait(interface_admin, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
- self.driver.get(self.shell_url("authentik_core:user-details"))
+ self.driver.get(self.if_admin_url("authentik_core:user-details"))
user = User.objects.get(username="foo")
self.assertEqual(user.username, "foo")
@@ -196,7 +196,7 @@ class TestFlowsEnroll(SeleniumTestCase):
)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
- self.driver.get(self.shell_url("authentik_core:user-details"))
+ self.driver.get(self.if_admin_url("authentik_core:user-details"))
self.assert_user(User.objects.get(username="foo"))
diff --git a/tests/e2e/test_flows_login.py b/tests/e2e/test_flows_login.py
index 227890898..2e663a179 100644
--- a/tests/e2e/test_flows_login.py
+++ b/tests/e2e/test_flows_login.py
@@ -14,7 +14,12 @@ class TestFlowsLogin(SeleniumTestCase):
@apply_migration("authentik_flows", "0008_default_flows")
def test_login(self):
"""test default login flow"""
- self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
+ self.driver.get(
+ self.url(
+ "authentik_core:if-flow",
+ flow_slug="default-authentication-flow",
+ )
+ )
self.login()
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER())
diff --git a/tests/e2e/test_flows_stage_setup.py b/tests/e2e/test_flows_stage_setup.py
index b53aead6a..43e5efa9e 100644
--- a/tests/e2e/test_flows_stage_setup.py
+++ b/tests/e2e/test_flows_stage_setup.py
@@ -35,10 +35,13 @@ class TestFlowsStageSetup(SeleniumTestCase):
new_password = generate_client_secret()
self.driver.get(
- f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
+ self.url(
+ "authentik_core:if-flow",
+ flow_slug="default-authentication-flow",
+ )
)
self.login()
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(
self.url(
@@ -60,7 +63,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
By.CSS_SELECTOR, "input[name=password_repeat]"
).send_keys(Keys.ENTER)
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
# Because USER() is cached, we need to get the user manually here
user = User.objects.get(username=USER().username)
self.assertTrue(user.check_password(new_password))
diff --git a/tests/e2e/test_source_oauth.py b/tests/e2e/test_source_oauth.py
index a87a81e41..8c7541106 100644
--- a/tests/e2e/test_source_oauth.py
+++ b/tests/e2e/test_source_oauth.py
@@ -159,7 +159,7 @@ class TestSourceOAuth2(SeleniumTestCase):
)
# Wait until we've logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
self.assertEqual(
@@ -254,7 +254,7 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# Wait until we've logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
self.assertEqual(
@@ -358,7 +358,7 @@ class TestSourceOAuth1(SeleniumTestCase):
# Wait until we've loaded the user info page
sleep(2)
# Wait until we've logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
self.assertEqual(
diff --git a/tests/e2e/test_source_saml.py b/tests/e2e/test_source_saml.py
index 0f87c83f1..f1a71a6f0 100644
--- a/tests/e2e/test_source_saml.py
+++ b/tests/e2e/test_source_saml.py
@@ -145,7 +145,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page
@@ -207,7 +207,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page
@@ -267,7 +267,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
- self.wait_for_url(self.shell_url("/library"))
+ self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page
diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py
index 2cec7f743..8e5eb81a8 100644
--- a/tests/e2e/utils.py
+++ b/tests/e2e/utils.py
@@ -109,9 +109,9 @@ class SeleniumTestCase(StaticLiveServerTestCase):
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
return self.live_server_url + reverse(view, kwargs=kwargs)
- def shell_url(self, view) -> str:
+ def if_admin_url(self, view) -> str:
"""same as self.url() but show URL in shell"""
- return f"{self.live_server_url}/#{view}"
+ return f"{self.live_server_url}/if/admin/#{view}"
def get_shadow_root(
self, selector: str, container: Optional[WebElement] = None
@@ -156,7 +156,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
"""Check users/me API and assert it matches expected_user"""
self.driver.get(self.url("authentik_api:user-me") + "?format=json")
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text
- user = UserSerializer(data=json.loads(user_json))
+ user = UserSerializer(data=json.loads(user_json)["user"])
user.is_valid()
self.assertEqual(user["username"].value, expected_user.username)
self.assertEqual(user["name"].value, expected_user.name)
diff --git a/web/nginx.conf b/web/nginx.conf
index 53774de8e..b25b6d003 100644
--- a/web/nginx.conf
+++ b/web/nginx.conf
@@ -73,6 +73,7 @@ http {
server_name _;
charset utf-8;
root /usr/share/nginx/html;
+ index index.html;
location / {
access_log /dev/stdout json_combined;
@@ -83,6 +84,15 @@ http {
add_header X-authentik-version "2021.3.4";
add_header Vary X-authentik-version;
}
+
+ location /if/admin {
+ root /usr/share/nginx/html/static/dist;
+ try_files $uri /static/dist/if/admin/index.html;
+ }
+ location /if/flow {
+ root /usr/share/nginx/html/static/dist;
+ try_files $uri /static/dist/if/flow/index.html;
+ }
}
}
diff --git a/web/rollup.config.js b/web/rollup.config.js
index 56ff67374..13a9e2bd6 100644
--- a/web/rollup.config.js
+++ b/web/rollup.config.js
@@ -14,7 +14,8 @@ const resources = [
{ src: "src/authentik.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
- { src: "src/index.html", dest: "dist" },
+ { src: "src/interfaces/admin/index.html", dest: "dist/if/admin/" },
+ { src: "src/interfaces/flow/index.html", dest: "dist/if/flow/" },
{ src: "src/assets/*", dest: "dist/assets" },
{ src: "./icons/*", dest: "dist/assets/icons" },
];
diff --git a/web/src/api/Config.ts b/web/src/api/Config.ts
index 2ad38589a..fbb388c43 100644
--- a/web/src/api/Config.ts
+++ b/web/src/api/Config.ts
@@ -9,6 +9,7 @@ export const DEFAULT_CONFIG = new Configuration({
basePath: "/api/v2beta",
headers: {
"X-CSRFToken": getCookie("authentik_csrf"),
+ "X-Authentik-Prevent-Basic": "true"
}
});
diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts
index 0f913a967..f134d8272 100644
--- a/web/src/api/Users.ts
+++ b/web/src/api/Users.ts
@@ -1,10 +1,15 @@
-import { CoreApi, User } from "authentik-api";
+import { CoreApi, SessionUser } from "authentik-api";
import { DEFAULT_CONFIG } from "./Config";
-let _globalMePromise: Promise;
-export function me(): Promise {
+let _globalMePromise: Promise;
+export function me(): Promise {
if (!_globalMePromise) {
- _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({});
+ _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({}).catch((ex) => {
+ if (ex.status === 401 || ex.status === 403) {
+ window.location.assign("/");
+ }
+ return ex;
+ });
}
return _globalMePromise;
}
diff --git a/web/src/authentik.css b/web/src/authentik.css
index c51fae559..7d55c4044 100644
--- a/web/src/authentik.css
+++ b/web/src/authentik.css
@@ -56,7 +56,7 @@ html > form > input {
/* ensure background on non-flow pages match */
.pf-c-background-image::before {
- background-image: url("/static/dist/assets/images/flow_background.jpg");
+ background-image: var(--ak-flow-background, url("/static/dist/assets/images/flow_background.jpg"));
background-position: center;
}
diff --git a/web/src/elements/Banner.ts b/web/src/elements/Banner.ts
new file mode 100644
index 000000000..0c8d651ec
--- /dev/null
+++ b/web/src/elements/Banner.ts
@@ -0,0 +1,27 @@
+import { customElement, CSSResult, html, LitElement, property, TemplateResult } from "lit-element";
+import PFBase from "@patternfly/patternfly/patternfly-base.css";
+import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
+import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
+import AKGlobal from "../authentik.css";
+
+@customElement("ak-banner")
+export class Banner extends LitElement {
+
+ @property()
+ level = "pf-m-warning";
+
+ static get styles(): CSSResult[] {
+ return [PFBase, PFBanner, PFFlex, AKGlobal];
+ }
+
+ render(): TemplateResult {
+ return html``;
+ }
+
+}
diff --git a/web/src/elements/sidebar/SidebarUser.ts b/web/src/elements/sidebar/SidebarUser.ts
index 8ab0a16ff..f96a3a0e6 100644
--- a/web/src/elements/sidebar/SidebarUser.ts
+++ b/web/src/elements/sidebar/SidebarUser.ts
@@ -36,7 +36,7 @@ export class SidebarUser extends LitElement {
return html`
${until(me().then((u) => {
- return html``;
+ return html``;
}), html``)}
diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts
index 87e5e8cd2..75c2471cc 100644
--- a/web/src/flows/FlowExecutor.ts
+++ b/web/src/flows/FlowExecutor.ts
@@ -83,7 +83,13 @@ export class FlowExecutor extends LitElement implements StageHost {
this.addEventListener("ak-flow-submit", () => {
this.submit();
});
- this.flowSlug = window.location.pathname.split("/")[2];
+ this.flowSlug = window.location.pathname.split("/")[3];
+ }
+
+ setBackground(url: string): void {
+ this.shadowRoot?.querySelectorAll(".pf-c-background-image").forEach((bg) => {
+ bg.style.setProperty("--ak-flow-background", `url('${url}')`);
+ });
}
submit(formData?: T): Promise {
@@ -95,6 +101,9 @@ export class FlowExecutor extends LitElement implements StageHost {
return challengeRaw.raw.json();
}).then((data) => {
this.challenge = data;
+ if (this.challenge?.background) {
+ this.setBackground(this.challenge.background);
+ }
}).catch((e) => {
this.errorMessage(e);
}).finally(() => {
@@ -113,6 +122,9 @@ export class FlowExecutor extends LitElement implements StageHost {
return challengeRaw.raw.json();
}).then((challenge) => {
this.challenge = challenge as Challenge;
+ if (this.challenge?.background) {
+ this.setBackground(this.challenge.background);
+ }
}).catch((e) => {
// Catch JSON or Update errors
this.errorMessage(e);
diff --git a/web/src/index.html b/web/src/index.html
deleted file mode 100644
index f17fe6b45..000000000
--- a/web/src/index.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
- authentik
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts
index 9a9821fdc..d63d8da89 100644
--- a/web/src/interfaces/AdminInterface.ts
+++ b/web/src/interfaces/AdminInterface.ts
@@ -10,7 +10,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/system-tasks"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
new SidebarItem("Events").children(
new SidebarItem("Log", "/events/log").activeWhen(
@@ -19,7 +19,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Notification Rules", "/events/rules"),
new SidebarItem("Notification Transports", "/events/transports"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
new SidebarItem("Resources").children(
new SidebarItem("Applications", "/core/applications").activeWhen(
@@ -34,13 +34,13 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Outposts", "/outpost/outposts"),
new SidebarItem("Outpost Service Connections", "/outpost/service-connections"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
new SidebarItem("Customisation").children(
new SidebarItem("Policies", "/policy/policies"),
new SidebarItem("Property Mappings", "/core/property-mappings"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
new SidebarItem("Flows").children(
new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?${SLUG_REGEX})$`),
@@ -48,7 +48,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Prompts", "/flow/stages/prompts"),
new SidebarItem("Invitations", "/flow/stages/invitations"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
new SidebarItem("Identity & Cryptography").children(
new SidebarItem("User", "/identity/users").activeWhen(`^/identity/users/(?${ID_REGEX})$`),
@@ -56,7 +56,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/core/tokens"),
).when((): Promise => {
- return me().then(u => u.isSuperuser||false);
+ return me().then(u => u.user.isSuperuser||false);
}),
];
diff --git a/web/src/interfaces/Interface.ts b/web/src/interfaces/Interface.ts
index b3ec00786..a4a96b84c 100644
--- a/web/src/interfaces/Interface.ts
+++ b/web/src/interfaces/Interface.ts
@@ -10,6 +10,10 @@ import "../elements/router/RouterOutlet";
import "../elements/messages/MessageContainer";
import "../elements/sidebar/SidebarHamburger";
import "../elements/notifications/NotificationDrawer";
+import "../elements/Banner";
+import { until } from "lit-html/directives/until";
+import { me } from "../api/Users";
+import { gettext } from "django";
export abstract class Interface extends LitElement {
@property({type: Boolean})
@@ -44,6 +48,17 @@ export abstract class Interface extends LitElement {
render(): TemplateResult {
return html`
+ ${until(me().then((u) => {
+ if (u.original) {
+ return html`
+ ${gettext(`You're currently impersonating ${u.user.username}.`)}
+
+ ${gettext("Stop impersonation")}
+
+ `;
+ }
+ return html``;
+ }))}
diff --git a/web/src/interfaces/admin/index.html b/web/src/interfaces/admin/index.html
new file mode 100644
index 000000000..22723968f
--- /dev/null
+++ b/web/src/interfaces/admin/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/interfaces/flow/index.html b/web/src/interfaces/flow/index.html
new file mode 100644
index 000000000..8078f0886
--- /dev/null
+++ b/web/src/interfaces/flow/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/pages/LibraryPage.ts b/web/src/pages/LibraryPage.ts
index 21e797d69..e46e6dd4c 100644
--- a/web/src/pages/LibraryPage.ts
+++ b/web/src/pages/LibraryPage.ts
@@ -61,7 +61,7 @@ export class LibraryApplication extends LitElement {
? html`
`
: html`
`}
${until(me().then((u) => {
- if (!u.isSuperuser) return html``;
+ if (!u.user.isSuperuser) return html``;
return html`
diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts
index 8418330b8..cdc0b4360 100644
--- a/web/src/pages/users/UserListPage.ts
+++ b/web/src/pages/users/UserListPage.ts
@@ -110,7 +110,7 @@ export class UserListPage extends TablePage {
${gettext("Reset Password")}
-
+
${gettext("Impersonate")}
`,
];