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:
parent
936e2fb4e2
commit
fe7f23238c
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"'
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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"},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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: /
|
||||||
|
|
|
@ -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
|
||||||
|
|
24
swagger.yaml
24
swagger.yaml
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" },
|
||||||
];
|
];
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
|
@ -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);
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>`,
|
||||||
];
|
];
|
||||||
|
|
Reference in New Issue