Static SPA (#648)

* core: initial migration to /if

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: move jsi18n to api

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* tests: fix static URLs in tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: add new html files to rollup

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix rollup config and nginx config

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: add Impersonation support to user API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: add banner for impersonation

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* tests: fix test_user function for new User API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: add background to API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: set background from flow API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: make root view login_required for redirect

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: redirect to root-redirect instead of if-admin direct

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* api: add header to prevent Authorization Basic prompt in browser

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: redirect to root when user/me request fails

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-03-22 13:44:17 +01:00 committed by GitHub
parent 936e2fb4e2
commit fe7f23238c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 332 additions and 233 deletions

View File

@ -33,7 +33,7 @@ class CertificateKeyPairCreateView(
permission_required = "authentik_crypto.add_certificatekeypair" permission_required = "authentik_crypto.add_certificatekeypair"
template_name = "generic/create.html" 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") success_message = _("Successfully created Certificate-Key Pair")
@ -50,7 +50,7 @@ class CertificateKeyPairGenerateView(
permission_required = "authentik_crypto.add_certificatekeypair" permission_required = "authentik_crypto.add_certificatekeypair"
template_name = "administration/certificatekeypair/generate.html" 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") success_message = _("Successfully generated Certificate-Key Pair")
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse: def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
@ -77,5 +77,5 @@ class CertificateKeyPairUpdateView(
permission_required = "authentik_crypto.change_certificatekeypair" permission_required = "authentik_crypto.change_certificatekeypair"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Certificate-Key Pair")

View File

@ -34,7 +34,7 @@ class FlowCreateView(
permission_required = "authentik_flows.add_flow" permission_required = "authentik_flows.add_flow"
template_name = "generic/create.html" 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") success_message = _("Successfully created Flow")
@ -51,7 +51,7 @@ class FlowUpdateView(
permission_required = "authentik_flows.change_flow" permission_required = "authentik_flows.change_flow"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Flow")
@ -79,7 +79,7 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
), ),
) )
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
self.request.GET, self.request.GET,
flow_slug=flow.slug, flow_slug=flow.slug,
) )
@ -91,7 +91,7 @@ class FlowImportView(LoginRequiredMixin, FormView):
form_class = FlowImportForm form_class = FlowImportForm
template_name = "administration/flow/import.html" 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): def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser: if not request.user.is_superuser:

View File

@ -27,7 +27,7 @@ class GroupCreateView(
permission_required = "authentik_core.add_group" permission_required = "authentik_core.add_group"
template_name = "generic/create.html" 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") success_message = _("Successfully created Group")
@ -44,5 +44,5 @@ class GroupUpdateView(
permission_required = "authentik_core.change_group" permission_required = "authentik_core.change_group"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Group")

View File

@ -24,7 +24,7 @@ class OutpostServiceConnectionCreateView(
permission_required = "authentik_outposts.add_outpostserviceconnection" permission_required = "authentik_outposts.add_outpostserviceconnection"
template_name = "generic/create.html" 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") success_message = _("Successfully created Outpost Service Connection")
@ -40,5 +40,5 @@ class OutpostServiceConnectionUpdateView(
permission_required = "authentik_outposts.change_outpostserviceconnection" permission_required = "authentik_outposts.change_outpostserviceconnection"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Outpost Service Connection")

View File

@ -31,7 +31,7 @@ class PolicyCreateView(
permission_required = "authentik_policies.add_policy" permission_required = "authentik_policies.add_policy"
template_name = "generic/create.html" 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") success_message = _("Successfully created Policy")
@ -47,7 +47,7 @@ class PolicyUpdateView(
permission_required = "authentik_policies.change_policy" permission_required = "authentik_policies.change_policy"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Policy")

View File

@ -30,7 +30,7 @@ class PolicyBindingCreateView(
form_class = PolicyBindingForm form_class = PolicyBindingForm
template_name = "generic/create.html" 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") success_message = _("Successfully created PolicyBinding")
def get_initial(self) -> dict[str, Any]: def get_initial(self) -> dict[str, Any]:
@ -63,5 +63,5 @@ class PolicyBindingUpdateView(
form_class = PolicyBindingForm form_class = PolicyBindingForm
template_name = "generic/update.html" 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") success_message = _("Successfully updated PolicyBinding")

View File

@ -24,7 +24,7 @@ class StageCreateView(
template_name = "generic/create.html" template_name = "generic/create.html"
permission_required = "authentik_flows.add_stage" 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") success_message = _("Successfully created Stage")
@ -39,5 +39,5 @@ class StageUpdateView(
model = Stage model = Stage
permission_required = "authentik_flows.update_application" permission_required = "authentik_flows.update_application"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Stage")

View File

@ -30,7 +30,7 @@ class StageBindingCreateView(
form_class = FlowStageBindingForm form_class = FlowStageBindingForm
template_name = "generic/create.html" 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") success_message = _("Successfully created StageBinding")
def get_initial(self) -> dict[str, Any]: def get_initial(self) -> dict[str, Any]:
@ -61,5 +61,5 @@ class StageBindingUpdateView(
form_class = FlowStageBindingForm form_class = FlowStageBindingForm
template_name = "generic/update.html" 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") success_message = _("Successfully updated StageBinding")

View File

@ -26,7 +26,7 @@ class InvitationCreateView(
permission_required = "authentik_stages_invitation.add_invitation" permission_required = "authentik_stages_invitation.add_invitation"
template_name = "generic/create.html" 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") success_message = _("Successfully created Invitation")
def form_valid(self, form): def form_valid(self, form):

View File

@ -27,7 +27,7 @@ class PromptCreateView(
permission_required = "authentik_stages_prompt.add_prompt" permission_required = "authentik_stages_prompt.add_prompt"
template_name = "generic/create.html" 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") success_message = _("Successfully created Prompt")
@ -44,5 +44,5 @@ class PromptUpdateView(
permission_required = "authentik_stages_prompt.change_prompt" permission_required = "authentik_stages_prompt.change_prompt"
template_name = "generic/update.html" 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") success_message = _("Successfully updated Prompt")

View File

@ -31,7 +31,7 @@ class UserCreateView(
permission_required = "authentik_core.add_user" permission_required = "authentik_core.add_user"
template_name = "generic/create.html" 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") 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 # By default the object's name is user which is used by other checks
context_object_name = "object" context_object_name = "object"
template_name = "generic/update.html" 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") success_message = _("Successfully updated User")

View File

@ -14,7 +14,7 @@ from authentik.lib.views import CreateAssignPermView
class DeleteMessageView(SuccessMessageMixin, DeleteView): class DeleteMessageView(SuccessMessageMixin, DeleteView):
"""DeleteView which shows `self.success_message` on successful deletion""" """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): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -10,6 +10,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User
LOGGER = get_logger() LOGGER = get_logger()
X_AUTHENTIK_PREVENT_BASIC_HEADER = "HTTP_X_AUTHENTIK_PREVENT_BASIC"
def token_from_header(raw_header: bytes) -> Optional[Token]: def token_from_header(raw_header: bytes) -> Optional[Token]:
@ -55,4 +56,6 @@ class AuthentikTokenAuthentication(BaseAuthentication):
return (token.user, None) return (token.user, None)
def authenticate_header(self, request: Request) -> str: def authenticate_header(self, request: Request) -> str:
if X_AUTHENTIK_PREVENT_BASIC_HEADER in request._request.META:
return ""
return 'Basic realm="authentik"' return 'Basic realm="authentik"'

View File

@ -1,8 +1,10 @@
"""authentik api urls""" """authentik api urls"""
from django.urls import include, path from django.urls import include, path
from django.views.i18n import JavaScriptCatalog
from authentik.api.v2.urls import urlpatterns as v2_urls from authentik.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [ urlpatterns = [
path("v2beta/", include(v2_urls)), path("v2beta/", include(v2_urls)),
path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
] ]

View File

@ -10,6 +10,10 @@ from rest_framework.serializers import BooleanField, ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h 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.core.models import User
from authentik.events.models import EventAction 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): class UserMetricsSerializer(Serializer):
"""User Metrics""" """User Metrics"""
@ -83,12 +101,20 @@ class UserViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
return User.objects.all().exclude(pk=get_anonymous_user().pk) 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) @action(detail=False)
# pylint: disable=invalid-name # pylint: disable=invalid-name
def me(self, request: Request) -> Response: def me(self, request: Request) -> Response:
"""Get information about current user""" """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)}) @swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=False) @action(detail=False)

View File

@ -13,21 +13,11 @@
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script> <script src="{% url 'authentik_api:javascript-catalog' %}?v={{ ak_version }}"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% if 'authentik_impersonate_user' in request.session %}
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
<div class="pf-u-display-none pf-u-display-block-on-lg">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="{% url 'authentik_core:impersonate-end' %}?back={{ request.get_full_path }}" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</div>
</div>
</div>
{% endif %}
{% block body %} {% block body %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@ -1,4 +1,6 @@
"""impersonation tests""" """impersonation tests"""
from json import loads
from django.test.testcases import TestCase from django.test.testcases import TestCase
from django.urls import reverse from django.urls import reverse
@ -25,14 +27,16 @@ class TestImpersonation(TestCase):
) )
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
self.assertIn(self.other_user.username, response.content.decode()) response_body = loads(response.content.decode())
self.assertNotIn(self.akadmin.username, 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")) self.client.get(reverse("authentik_core:impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
self.assertNotIn(self.other_user.username, response.content.decode()) response_body = loads(response.content.decode())
self.assertIn(self.akadmin.username, response.content.decode()) self.assertEqual(response_body["user"]["username"], self.akadmin.username)
self.assertNotIn("original", response_body)
def test_impersonate_denied(self): def test_impersonate_denied(self):
"""test impersonation without permissions""" """test impersonation without permissions"""
@ -45,12 +49,12 @@ class TestImpersonation(TestCase):
) )
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
self.assertIn(self.other_user.username, response.content.decode()) response_body = loads(response.content.decode())
self.assertNotIn(self.akadmin.username, response.content.decode()) self.assertEqual(response_body["user"]["username"], self.other_user.username)
def test_un_impersonate_empty(self): def test_un_impersonate_empty(self):
"""test un-impersonation without impersonating first""" """test un-impersonation without impersonating first"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end")) response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertRedirects(response, reverse("authentik_core:shell")) self.assertRedirects(response, reverse("authentik_core:if-admin"))

View File

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

View File

@ -1,10 +1,19 @@
"""authentik URL Configuration""" """authentik URL Configuration"""
from django.contrib.auth.decorators import login_required
from django.urls import path 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 = [ 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 # User views
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"), path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
path( path(
@ -28,4 +37,15 @@ urlpatterns = [
impersonate.ImpersonateEndView.as_view(), impersonate.ImpersonateEndView.as_view(),
name="impersonate-end", name="impersonate-end",
), ),
# Interfaces
path(
"if/admin/",
ensure_csrf_cookie(TemplateView.as_view(template_name="shell.html")),
name="if-admin",
),
path(
"if/flow/<slug:flow_slug>/",
ensure_csrf_cookie(FlowExecutorShellView.as_view()),
name="if-flow",
),
] ]

View File

@ -33,7 +33,7 @@ class ImpersonateInitView(View):
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) 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): class ImpersonateEndView(View):
@ -46,7 +46,7 @@ class ImpersonateEndView(View):
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
): ):
LOGGER.debug("Can't end impersonation", user=request.user) 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] 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) Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("authentik_core:shell") return redirect("authentik_core:root-redirect")

View File

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

View File

@ -43,6 +43,7 @@ class Challenge(Serializer):
) )
component = CharField(required=False) component = CharField(required=False)
title = CharField(required=False) title = CharField(required=False)
background = CharField(required=False)
response_errors = DictField( response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=False, required=False child=ErrorDetailSerializer(many=True), allow_empty=False, required=False

View File

@ -79,6 +79,8 @@ class ChallengeStageView(StageView):
challenge = self.get_challenge(*args, **kwargs) challenge = self.get_challenge(*args, **kwargs)
if "title" not in challenge.initial_data: if "title" not in challenge.initial_data:
challenge.initial_data["title"] = self.executor.flow.title 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 isinstance(challenge, WithUserInfoChallenge):
# If there's a pending user, update the `username` field # If there's a pending user, update the `username` field
# this field is only used by password managers. # this field is only used by password managers.

View File

@ -109,7 +109,7 @@ class TestFlowExecutor(TestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
) )
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:shell")) self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.to_stage_response",
@ -128,7 +128,7 @@ class TestFlowExecutor(TestCase):
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}") response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
self.assertEqual(response.status_code, 302) 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): def test_multi_stage_flow(self):
"""Test a full flow with multiple stages""" """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 # We do this request without the patch, so the policy results in false
response = self.client.post(exec_url) response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302) 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): def test_reevaluate_remove_middle(self):
"""Test planner with re-evaluate (middle stage is removed)""" """Test planner with re-evaluate (middle stage is removed)"""
@ -283,7 +283,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
def test_reevaluate_keep(self): def test_reevaluate_keep(self):
@ -360,7 +360,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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): def test_reevaluate_remove_consecutive(self):
@ -408,6 +408,7 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{ {
"background": flow.background.url,
"type": ChallengeTypes.native.value, "type": ChallengeTypes.native.value,
"component": "", "component": "",
"title": binding.stage.name, "title": binding.stage.name,
@ -438,6 +439,7 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{ {
"background": flow.background.url,
"type": ChallengeTypes.native.value, "type": ChallengeTypes.native.value,
"component": "", "component": "",
"title": binding4.stage.name, "title": binding4.stage.name,
@ -450,7 +452,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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): def test_stageview_user_identifier(self):

View File

@ -22,7 +22,7 @@ class TestHelperView(TestCase):
reverse("authentik_flows:default-invalidation"), reverse("authentik_flows:default-invalidation"),
) )
expected_url = reverse( 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.status_code, 302)
self.assertEqual(response.url, expected_url) self.assertEqual(response.url, expected_url)
@ -41,7 +41,7 @@ class TestHelperView(TestCase):
reverse("authentik_flows:default-invalidation"), reverse("authentik_flows:default-invalidation"),
) )
expected_url = reverse( 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.status_code, 302)
self.assertEqual(response.url, expected_url) self.assertEqual(response.url, expected_url)

View File

@ -1,14 +1,9 @@
"""flow urls""" """flow urls"""
from django.urls import path 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.models import FlowDesignation
from authentik.flows.views import ( from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow
CancelView,
ConfigureFlowInitView,
FlowExecutorShellView,
ToDefaultFlow,
)
urlpatterns = [ urlpatterns = [
path( path(
@ -44,7 +39,7 @@ urlpatterns = [
), ),
path( path(
"<slug:flow_slug>/", "<slug:flow_slug>/",
ensure_csrf_cookie(FlowExecutorShellView.as_view()), RedirectView.as_view(pattern_name="authentik_core:if-flow"),
name="flow-executor-shell", name="flow-executor-shell",
), ),
] ]

View File

@ -173,7 +173,7 @@ class FlowExecutorView(APIView):
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT) next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
if not next_param: if not next_param:
next_param = self.request.session.get(SESSION_KEY_GET, {}).get( 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() self.cancel()
return to_stage_response(self.request, redirect_with_qs(next_param)) return to_stage_response(self.request, redirect_with_qs(next_param))
@ -263,11 +263,8 @@ class FlowExecutorShellView(TemplateView):
template_name = "flows/shell.html" template_name = "flows/shell.html"
def get_context_data(self, **kwargs) -> dict[str, Any]: 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 self.request.session[SESSION_KEY_GET] = self.request.GET
return kwargs return super().get_context_data(**kwargs)
class CancelView(View): class CancelView(View):
@ -278,7 +275,7 @@ class CancelView(View):
if SESSION_KEY_PLAN in request.session: if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN] del request.session[SESSION_KEY_PLAN]
LOGGER.debug("Canceled current plan") LOGGER.debug("Canceled current plan")
return redirect("authentik_core:shell") return redirect("authentik_core:root-redirect")
class ToDefaultFlow(View): class ToDefaultFlow(View):
@ -300,7 +297,7 @@ class ToDefaultFlow(View):
) )
del self.request.session[SESSION_KEY_PLAN] del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs( 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 request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
self.request.GET, self.request.GET,
flow_slug=stage.configure_flow.slug, flow_slug=stage.configure_flow.slug,
) )

View File

@ -32,7 +32,7 @@ You've logged out of {{ application }}.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<a id="ak-back-home" href="{% url 'authentik_core:shell' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a> <a id="ak-back-home" href="{% url 'authentik_core:if-admin' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a> <a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>

View File

@ -464,7 +464,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
plan.append(in_memory_stage(OAuthFulfillmentStage)) plan.append(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
self.request.GET, self.request.GET,
flow_slug=self.provider.authorization_flow.slug, flow_slug=self.provider.authorization_flow.slug,
) )

View File

@ -82,7 +82,7 @@ class SAMLSSOView(PolicyAccessView):
plan.append(in_memory_stage(SAMLFlowFinalView)) plan.append(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
request.GET, request.GET,
flow_slug=self.provider.authorization_flow.slug, flow_slug=self.provider.authorization_flow.slug,
) )

View File

@ -21,4 +21,4 @@ class UseTokenView(View):
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend") login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
token.delete() token.delete()
messages.warning(request, _("Used recovery-link to authenticate.")) messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("authentik_core:shell") return redirect("authentik_core:if-admin")

View File

@ -4,7 +4,6 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.views.i18n import JavaScriptCatalog
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.views import error from authentik.core.views import error
@ -59,7 +58,6 @@ urlpatterns += [
path("metrics/", MetricsView.as_view(), name="metrics"), path("metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"), path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"), path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -141,7 +141,7 @@ class OAuthCallback(OAuthClientMixin, View):
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
# authorize application # authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( 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( kwargs.update(
{ {
@ -159,7 +159,7 @@ class OAuthCallback(OAuthClientMixin, View):
plan = planner.plan(self.request, kwargs) plan = planner.plan(self.request, kwargs)
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
self.request.GET, self.request.GET,
flow_slug=flow.slug, flow_slug=flow.slug,
) )
@ -244,7 +244,7 @@ class OAuthCallback(OAuthClientMixin, View):
plan.append(in_memory_stage(PostUserEnrollmentStage)) plan.append(in_memory_stage(PostUserEnrollmentStage))
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
self.request.GET, self.request.GET,
flow_slug=source.enrollment_flow.slug, flow_slug=source.enrollment_flow.slug,
) )

View File

@ -195,7 +195,7 @@ class ResponseProcessor:
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
# authorize application # authorize application
final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get( 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(): if matching_users.exists():
# User exists already, switch to authentication flow # User exists already, switch to authentication flow
@ -221,7 +221,7 @@ class ResponseProcessor:
kwargs[PLAN_CONTEXT_SOURCE] = self._source kwargs[PLAN_CONTEXT_SOURCE] = self._source
request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs) request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs)
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
request.GET, request.GET,
flow_slug=flow.slug, flow_slug=flow.slug,
) )

View File

@ -54,5 +54,5 @@ class TestCaptchaStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )

View File

@ -51,7 +51,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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()) self.assertFalse(UserConsent.objects.filter(user=self.user).exists())
@ -82,7 +82,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
self.assertTrue( self.assertTrue(
UserConsent.objects.filter( UserConsent.objects.filter(
@ -119,7 +119,7 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
self.assertTrue( self.assertTrue(
UserConsent.objects.filter( UserConsent.objects.filter(

View File

@ -47,7 +47,7 @@ class TestDummyStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
def test_form(self): def test_form(self):

View File

@ -45,7 +45,7 @@ class EmailStageView(ChallengeStageView):
def get_full_url(self, **kwargs) -> str: def get_full_url(self, **kwargs) -> str:
"""Get full URL to be used in template""" """Get full URL to be used in template"""
base_url = reverse( base_url = reverse(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
kwargs={"flow_slug": self.executor.flow.slug}, kwargs={"flow_slug": self.executor.flow.slug},
) )
relative_url = f"{base_url}?{urlencode(kwargs)}" relative_url = f"{base_url}?{urlencode(kwargs)}"

View File

@ -109,7 +109,7 @@ class TestEmailStage(TestCase):
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session # Call the executor shell to preseed the session
url = reverse( url = reverse(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
kwargs={"flow_slug": self.flow.slug}, kwargs={"flow_slug": self.flow.slug},
) )
token = Token.objects.get(user=self.user) token = Token.objects.get(user=self.user)
@ -126,7 +126,7 @@ class TestEmailStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
session = self.client.session session = self.client.session

View File

@ -95,12 +95,12 @@ class IdentificationStageView(ChallengeStageView):
# Check for related enrollment and recovery flow, add URL to view # Check for related enrollment and recovery flow, add URL to view
if current_stage.enrollment_flow: if current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse( challenge.initial_data["enroll_url"] = reverse(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
kwargs={"flow_slug": current_stage.enrollment_flow.slug}, kwargs={"flow_slug": current_stage.enrollment_flow.slug},
) )
if current_stage.recovery_flow: if current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse( challenge.initial_data["recovery_url"] = reverse(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
kwargs={"flow_slug": current_stage.recovery_flow.slug}, kwargs={"flow_slug": current_stage.recovery_flow.slug},
) )

View File

@ -53,7 +53,7 @@ class TestIdentificationStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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): def test_invalid_with_username(self):
@ -103,10 +103,14 @@ class TestIdentificationStage(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{ {
"background": flow.background.url,
"type": ChallengeTypes.native.value, "type": ChallengeTypes.native.value,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"input_type": "email", "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", "primary_action": "Log in",
"title": self.flow.title, "title": self.flow.title,
"sources": [ "sources": [
@ -142,10 +146,14 @@ class TestIdentificationStage(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{ {
"background": flow.background.url,
"type": ChallengeTypes.native.value, "type": ChallengeTypes.native.value,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"input_type": "email", "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", "primary_action": "Log in",
"title": self.flow.title, "title": self.flow.title,
"sources": [ "sources": [

View File

@ -85,7 +85,7 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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 self.stage.continue_flow_without_invitation = False
@ -124,5 +124,5 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )

View File

@ -85,7 +85,7 @@ class PasswordStageView(ChallengeStageView):
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists(): if recovery_flow.exists():
recover_url = reverse( recover_url = reverse(
"authentik_flows:flow-executor-shell", "authentik_core:if-flow",
kwargs={"flow_slug": recovery_flow.first().slug}, kwargs={"flow_slug": recovery_flow.first().slug},
) )
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri( challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(

View File

@ -110,7 +110,7 @@ class TestPasswordStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
def test_invalid_password(self): def test_invalid_password(self):

View File

@ -167,7 +167,7 @@ class TestPromptStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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 # Check that valid data has been saved

View File

@ -85,7 +85,7 @@ class TestUserDeleteStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), 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()) self.assertFalse(User.objects.filter(username=self.username).exists())

View File

@ -53,7 +53,7 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
@patch( @patch(

View File

@ -49,7 +49,7 @@ class TestUserLogoutStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
def test_form(self): def test_form(self):

View File

@ -61,7 +61,7 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
user_qs = User.objects.filter( user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"] username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
@ -98,7 +98,7 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:shell"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
user_qs = User.objects.filter( user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"] username=plan.context[PLAN_CONTEXT_PROMPT]["username"]

View File

@ -72,7 +72,7 @@ services:
labels: labels:
traefik.enable: 'true' traefik.enable: 'true'
traefik.docker.network: internal 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.tls: 'true'
traefik.http.routers.static-router.service: static-service traefik.http.routers.static-router.service: static-service
traefik.http.services.static-service.loadbalancer.healthcheck.path: / traefik.http.services.static-service.loadbalancer.healthcheck.path: /

View File

@ -36,6 +36,10 @@ spec:
backend: backend:
serviceName: {{ $fullName }}-static serviceName: {{ $fullName }}-static
servicePort: http servicePort: http
- path: /if/
backend:
serviceName: {{ $fullName }}-static
servicePort: http
- path: /media/ - path: /media/
backend: backend:
serviceName: {{ $fullName }}-static serviceName: {{ $fullName }}-static

View File

@ -1611,9 +1611,11 @@ paths:
type: integer type: integer
responses: responses:
'200': '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: schema:
$ref: '#/definitions/User' $ref: '#/definitions/SessionUser'
tags: tags:
- core - core
parameters: [] parameters: []
@ -11029,6 +11031,18 @@ definitions:
$ref: '#/definitions/User' $ref: '#/definitions/User'
application: application:
$ref: '#/definitions/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: UserMetrics:
description: User Metrics description: User Metrics
type: object type: object
@ -11533,6 +11547,10 @@ definitions:
title: Title title: Title
type: string type: string
minLength: 1 minLength: 1
background:
title: Background
type: string
minLength: 1
response_errors: response_errors:
title: Response errors title: Response errors
type: object type: object
@ -13332,7 +13350,7 @@ definitions:
type: string type: string
readOnly: true readOnly: true
Link: Link:
description: '' description: Links returned in Config API
type: object type: object
properties: properties:
href: href:

View File

@ -39,7 +39,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
target=flow, order=30, stage=AuthenticatorValidateStage.objects.create() 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() self.login()
# Get expected token # Get expected token
@ -59,7 +59,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys( code_stage.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys(
Keys.ENTER Keys.ENTER
) )
self.wait_for_url(self.shell_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER()) self.assert_user(USER())
@retry() @retry()
@ -70,10 +70,10 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test TOTP Setup stage""" """test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") 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.login()
self.wait_for_url(self.shell_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER()) self.assert_user(USER())
self.driver.get( self.driver.get(
@ -120,10 +120,10 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test Static OTP Setup stage""" """test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") 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.login()
self.wait_for_url(self.shell_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER()) self.assert_user(USER())
self.driver.get( self.driver.get(

View File

@ -97,7 +97,7 @@ class TestFlowsEnroll(SeleniumTestCase):
wait = WebDriverWait(interface_admin, self.wait_timeout) wait = WebDriverWait(interface_admin, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar"))) 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") user = User.objects.get(username="foo")
self.assertEqual(user.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"))) 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")) self.assert_user(User.objects.get(username="foo"))

View File

@ -14,7 +14,12 @@ class TestFlowsLogin(SeleniumTestCase):
@apply_migration("authentik_flows", "0008_default_flows") @apply_migration("authentik_flows", "0008_default_flows")
def test_login(self): def test_login(self):
"""test default login flow""" """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.login()
self.wait_for_url(self.shell_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.assert_user(USER()) self.assert_user(USER())

View File

@ -35,10 +35,13 @@ class TestFlowsStageSetup(SeleniumTestCase):
new_password = generate_client_secret() new_password = generate_client_secret()
self.driver.get( 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.login()
self.wait_for_url(self.shell_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get( self.driver.get(
self.url( self.url(
@ -60,7 +63,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
By.CSS_SELECTOR, "input[name=password_repeat]" By.CSS_SELECTOR, "input[name=password_repeat]"
).send_keys(Keys.ENTER) ).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 # Because USER() is cached, we need to get the user manually here
user = User.objects.get(username=USER().username) user = User.objects.get(username=USER().username)
self.assertTrue(user.check_password(new_password)) self.assertTrue(user.check_password(new_password))

View File

@ -159,7 +159,7 @@ class TestSourceOAuth2(SeleniumTestCase):
) )
# Wait until we've logged in # 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.driver.get(self.url("authentik_core:user-details"))
self.assertEqual( self.assertEqual(
@ -254,7 +254,7 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# Wait until we've logged in # 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.driver.get(self.url("authentik_core:user-details"))
self.assertEqual( self.assertEqual(
@ -358,7 +358,7 @@ class TestSourceOAuth1(SeleniumTestCase):
# Wait until we've loaded the user info page # Wait until we've loaded the user info page
sleep(2) sleep(2)
# Wait until we've logged in # 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.driver.get(self.url("authentik_core:user-details"))
self.assertEqual( self.assertEqual(

View File

@ -145,7 +145,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in # 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")) self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page # 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) self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in # 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")) self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page # 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) self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in # 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")) self.driver.get(self.url("authentik_core:user-details"))
# Wait until we've loaded the user info page # Wait until we've loaded the user info page

View File

@ -109,9 +109,9 @@ class SeleniumTestCase(StaticLiveServerTestCase):
"""reverse `view` with `**kwargs` into full URL using live_server_url""" """reverse `view` with `**kwargs` into full URL using live_server_url"""
return self.live_server_url + reverse(view, kwargs=kwargs) 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""" """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( def get_shadow_root(
self, selector: str, container: Optional[WebElement] = None self, selector: str, container: Optional[WebElement] = None
@ -156,7 +156,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
"""Check users/me API and assert it matches expected_user""" """Check users/me API and assert it matches expected_user"""
self.driver.get(self.url("authentik_api:user-me") + "?format=json") self.driver.get(self.url("authentik_api:user-me") + "?format=json")
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text 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() user.is_valid()
self.assertEqual(user["username"].value, expected_user.username) self.assertEqual(user["username"].value, expected_user.username)
self.assertEqual(user["name"].value, expected_user.name) self.assertEqual(user["name"].value, expected_user.name)

View File

@ -73,6 +73,7 @@ http {
server_name _; server_name _;
charset utf-8; charset utf-8;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html;
location / { location / {
access_log /dev/stdout json_combined; access_log /dev/stdout json_combined;
@ -83,6 +84,15 @@ http {
add_header X-authentik-version "2021.3.4"; add_header X-authentik-version "2021.3.4";
add_header Vary X-authentik-version; 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;
}
} }
} }

View File

@ -14,7 +14,8 @@ const resources = [
{ src: "src/authentik.css", dest: "dist/" }, { src: "src/authentik.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" }, { 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: "src/assets/*", dest: "dist/assets" },
{ src: "./icons/*", dest: "dist/assets/icons" }, { src: "./icons/*", dest: "dist/assets/icons" },
]; ];

View File

@ -9,6 +9,7 @@ export const DEFAULT_CONFIG = new Configuration({
basePath: "/api/v2beta", basePath: "/api/v2beta",
headers: { headers: {
"X-CSRFToken": getCookie("authentik_csrf"), "X-CSRFToken": getCookie("authentik_csrf"),
"X-Authentik-Prevent-Basic": "true"
} }
}); });

View File

@ -1,10 +1,15 @@
import { CoreApi, User } from "authentik-api"; import { CoreApi, SessionUser } from "authentik-api";
import { DEFAULT_CONFIG } from "./Config"; import { DEFAULT_CONFIG } from "./Config";
let _globalMePromise: Promise<User>; let _globalMePromise: Promise<SessionUser>;
export function me(): Promise<User> { export function me(): Promise<SessionUser> {
if (!_globalMePromise) { 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; return _globalMePromise;
} }

View File

@ -56,7 +56,7 @@ html > form > input {
/* ensure background on non-flow pages match */ /* ensure background on non-flow pages match */
.pf-c-background-image::before { .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; background-position: center;
} }

View File

@ -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`<div class="pf-c-banner ${this.level} pf-m-sticky">
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
<div class="pf-u-display-none pf-u-display-block-on-lg">
<slot></slot>
</div>
</div>
</div>`;
}
}

View File

@ -36,7 +36,7 @@ export class SidebarUser extends LitElement {
return html` return html`
<a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings"> <a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings">
${until(me().then((u) => { ${until(me().then((u) => {
return html`<img class="pf-c-avatar" src="${ifDefined(u.avatar)}" alt="" />`; return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`;
}), html``)} }), html``)}
</a> </a>
<ak-notification-trigger class="pf-c-nav__link user-notifications"> <ak-notification-trigger class="pf-c-nav__link user-notifications">

View File

@ -83,7 +83,13 @@ export class FlowExecutor extends LitElement implements StageHost {
this.addEventListener("ak-flow-submit", () => { this.addEventListener("ak-flow-submit", () => {
this.submit(); this.submit();
}); });
this.flowSlug = window.location.pathname.split("/")[2]; this.flowSlug = window.location.pathname.split("/")[3];
}
setBackground(url: string): void {
this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
});
} }
submit<T>(formData?: T): Promise<void> { submit<T>(formData?: T): Promise<void> {
@ -95,6 +101,9 @@ export class FlowExecutor extends LitElement implements StageHost {
return challengeRaw.raw.json(); return challengeRaw.raw.json();
}).then((data) => { }).then((data) => {
this.challenge = data; this.challenge = data;
if (this.challenge?.background) {
this.setBackground(this.challenge.background);
}
}).catch((e) => { }).catch((e) => {
this.errorMessage(e); this.errorMessage(e);
}).finally(() => { }).finally(() => {
@ -113,6 +122,9 @@ export class FlowExecutor extends LitElement implements StageHost {
return challengeRaw.raw.json(); return challengeRaw.raw.json();
}).then((challenge) => { }).then((challenge) => {
this.challenge = challenge as Challenge; this.challenge = challenge as Challenge;
if (this.challenge?.background) {
this.setBackground(this.challenge.background);
}
}).catch((e) => { }).catch((e) => {
// Catch JSON or Update errors // Catch JSON or Update errors
this.errorMessage(e); this.errorMessage(e);

View File

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>authentik</title>
<link rel="icon" type="image/png" href="/static/dist/assets/icons/icon.png" />
<link rel="shortcut icon" type="image/png" href="/static/dist/assets/icons/icon.png" />
<link
rel="stylesheet"
type="text/css"
href="/static/node_modules/%40patternfly/patternfly/patternfly.css"
/>
<link
rel="stylesheet"
type="text/css"
href="/static/node_modules/%40patternfly/patternfly/patternfly-addons.css"
/>
<link
rel="stylesheet"
type="text/css"
href="/static/node_modules/%40fortawesome/fontawesome-free/css/fontawesome.min.css"
/>
<link rel="stylesheet" type="text/css" href="/static/authentik/authentik.css" />
<script src="/static/dist/main.js" type="module"></script>
</head>
<body>
<ak-message-container></ak-message-container>
<div class="pf-c-page">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content"
>Skip to content</a
>
<ak-sidebar class="pf-c-page__sidebar"> </ak-sidebar>
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/library"
>
</ak-router-outlet>
</div>
</body>
</html>

View File

@ -10,7 +10,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Overview", "/administration/overview"), new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/system-tasks"), new SidebarItem("System Tasks", "/administration/system-tasks"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
new SidebarItem("Events").children( new SidebarItem("Events").children(
new SidebarItem("Log", "/events/log").activeWhen( new SidebarItem("Log", "/events/log").activeWhen(
@ -19,7 +19,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Notification Rules", "/events/rules"), new SidebarItem("Notification Rules", "/events/rules"),
new SidebarItem("Notification Transports", "/events/transports"), new SidebarItem("Notification Transports", "/events/transports"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
new SidebarItem("Resources").children( new SidebarItem("Resources").children(
new SidebarItem("Applications", "/core/applications").activeWhen( new SidebarItem("Applications", "/core/applications").activeWhen(
@ -34,13 +34,13 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Outposts", "/outpost/outposts"), new SidebarItem("Outposts", "/outpost/outposts"),
new SidebarItem("Outpost Service Connections", "/outpost/service-connections"), new SidebarItem("Outpost Service Connections", "/outpost/service-connections"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
new SidebarItem("Customisation").children( new SidebarItem("Customisation").children(
new SidebarItem("Policies", "/policy/policies"), new SidebarItem("Policies", "/policy/policies"),
new SidebarItem("Property Mappings", "/core/property-mappings"), new SidebarItem("Property Mappings", "/core/property-mappings"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
new SidebarItem("Flows").children( new SidebarItem("Flows").children(
new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?<slug>${SLUG_REGEX})$`),
@ -48,7 +48,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Prompts", "/flow/stages/prompts"), new SidebarItem("Prompts", "/flow/stages/prompts"),
new SidebarItem("Invitations", "/flow/stages/invitations"), new SidebarItem("Invitations", "/flow/stages/invitations"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
new SidebarItem("Identity & Cryptography").children( new SidebarItem("Identity & Cryptography").children(
new SidebarItem("User", "/identity/users").activeWhen(`^/identity/users/(?<id>${ID_REGEX})$`), new SidebarItem("User", "/identity/users").activeWhen(`^/identity/users/(?<id>${ID_REGEX})$`),
@ -56,7 +56,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/core/tokens"), new SidebarItem("Tokens", "/core/tokens"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return me().then(u => u.isSuperuser||false); return me().then(u => u.user.isSuperuser||false);
}), }),
]; ];

View File

@ -10,6 +10,10 @@ import "../elements/router/RouterOutlet";
import "../elements/messages/MessageContainer"; import "../elements/messages/MessageContainer";
import "../elements/sidebar/SidebarHamburger"; import "../elements/sidebar/SidebarHamburger";
import "../elements/notifications/NotificationDrawer"; 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 { export abstract class Interface extends LitElement {
@property({type: Boolean}) @property({type: Boolean})
@ -44,6 +48,17 @@ export abstract class Interface extends LitElement {
render(): TemplateResult { render(): TemplateResult {
return html` return html`
${until(me().then((u) => {
if (u.original) {
return html`<ak-banner>
${gettext(`You're currently impersonating ${u.user.username}.`)}
<a href=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}>
${gettext("Stop impersonation")}
</a>
</ak-banner>`;
}
return html``;
}))}
<div class="pf-c-page"> <div class="pf-c-page">
<ak-sidebar-hamburger> <ak-sidebar-hamburger>
</ak-sidebar-hamburger> </ak-sidebar-hamburger>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" type="image/png" href="/static/dist/assets/icons/icon.png?v=2021.3.4">
<link rel="shortcut icon" type="image/png" href="/static/dist/assets/icons/icon.png?v=2021.3.4">
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css?v=2021.3.4">
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css?v=2021.3.4">
<script src="/api/jsi18n/?v=2021.3.4"></script>
<script src="/static/dist/main.js?v=2021.3.4" type="module"></script>
</head>
<body>
<ak-message-container></ak-message-container>
<ak-interface-admin></ak-interface-admin>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" type="image/png" href="/static/dist/assets/icons/icon.png?v=2021.3.4">
<link rel="shortcut icon" type="image/png" href="/static/dist/assets/icons/icon.png?v=2021.3.4">
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css?v=2021.3.4">
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css?v=2021.3.4">
<script src="/api/jsi18n/?v=2021.3.4"></script>
<script src="/static/dist/flow.js?v=2021.3.4" type="module"></script>
</head>
<body>
<ak-flow-executor></ak-flow-executor>
</body>
</html>

View File

@ -61,7 +61,7 @@ export class LibraryApplication extends LitElement {
? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.metaIcon)}" alt="Application Icon"/>` ? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.metaIcon)}" alt="Application Icon"/>`
: html`<i class="fas fas fa-share-square"></i>`} : html`<i class="fas fas fa-share-square"></i>`}
${until(me().then((u) => { ${until(me().then((u) => {
if (!u.isSuperuser) return html``; if (!u.user.isSuperuser) return html``;
return html` return html`
<a href="#/core/applications/${this.application?.slug}"> <a href="#/core/applications/${this.application?.slug}">
<i class="fas fa-pencil-alt"></i> <i class="fas fa-pencil-alt"></i>

View File

@ -110,7 +110,7 @@ export class UserListPage extends TablePage<User> {
<ak-action-button method="GET" url="${AdminURLManager.users(`${item.pk}/reset/`)}"> <ak-action-button method="GET" url="${AdminURLManager.users(`${item.pk}/reset/`)}">
${gettext("Reset Password")} ${gettext("Reset Password")}
</ak-action-button> </ak-action-button>
<a class="pf-c-button pf-m-tertiary" href="${`-/impersonation/${item.pk}/`}"> <a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
${gettext("Impersonate")} ${gettext("Impersonate")}
</a>`, </a>`,
]; ];