flows: add API for user's stage settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
07142cab8b
commit
a6123cfbe4
|
@ -1,4 +1,6 @@
|
|||
"""Flow Stage API Views"""
|
||||
from typing import Iterable
|
||||
|
||||
from django.urls import reverse
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
|
@ -6,13 +8,17 @@ from rest_framework.request import Request
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.flows.api.flows import FlowSerializer
|
||||
from authentik.flows.challenge import Challenge
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Stage Serializer"""
|
||||
|
@ -64,3 +70,19 @@ class StageViewSet(ReadOnlyModelViewSet):
|
|||
)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@swagger_auto_schema(responses={200: Challenge(many=True)})
|
||||
@action(detail=False)
|
||||
def user_settings(self, request: Request) -> Response:
|
||||
"""Get all stages the user can configure"""
|
||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
|
||||
matching_stages: list[dict] = []
|
||||
for stage in _all_stages:
|
||||
user_settings = stage.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
stage_challenge = user_settings
|
||||
if not stage_challenge.is_valid():
|
||||
LOGGER.warning(stage_challenge.errors)
|
||||
matching_stages.append(stage_challenge.initial_data)
|
||||
return Response(matching_stages)
|
||||
|
|
|
@ -10,6 +10,7 @@ from model_utils.managers import InheritanceManager
|
|||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.challenge import Challenge
|
||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
|
@ -64,9 +65,9 @@ class Stage(SerializerModel):
|
|||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
def ui_user_settings(self) -> Optional[Challenge]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or a string with the URL to fetch."""
|
||||
user settings are available, or a challenge."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeTypes
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
|
@ -41,10 +42,16 @@ class AuthenticatorStaticStage(ConfigurableStage, Stage):
|
|||
return AuthenticatorStaticStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
def ui_user_settings(self) -> Optional[Challenge]:
|
||||
return Challenge(
|
||||
data={
|
||||
"type": ChallengeTypes.shell.value,
|
||||
"title": self._meta.verbose_name,
|
||||
"component": reverse(
|
||||
"authentik_stages_authenticator_static:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</ul>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23user" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_authenticator_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||
|
|
|
@ -44,4 +44,4 @@ class DisableView(LoginRequiredMixin, View):
|
|||
Event.new(
|
||||
"static_otp_disable", message="User disabled Static OTP Tokens."
|
||||
).from_http(request)
|
||||
return redirect("authentik_core:user-settings")
|
||||
return redirect("/")
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeTypes
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
|
@ -44,10 +45,16 @@ class AuthenticatorTOTPStage(ConfigurableStage, Stage):
|
|||
return AuthenticatorTOTPStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
def ui_user_settings(self) -> Optional[Challenge]:
|
||||
return Challenge(
|
||||
data={
|
||||
"type": ChallengeTypes.shell.value,
|
||||
"title": self._meta.verbose_name,
|
||||
"component": reverse(
|
||||
"authentik_stages_authenticator_totp:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<p>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23user" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_authenticator_totp:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||
|
|
|
@ -39,4 +39,4 @@ class DisableView(LoginRequiredMixin, View):
|
|||
Event.new("totp_disable", message="User disabled Time-based OTP.").from_http(
|
||||
request
|
||||
)
|
||||
return redirect("authentik_core:user-settings")
|
||||
return redirect("/")
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.views import View
|
|||
from django_otp.models import Device
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeTypes
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
|
@ -42,10 +43,16 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
|
|||
return AuthenticateWebAuthnStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
def ui_user_settings(self) -> Optional[Challenge]:
|
||||
return Challenge(
|
||||
data={
|
||||
"type": ChallengeTypes.shell.value,
|
||||
"title": self._meta.verbose_name,
|
||||
"component": reverse(
|
||||
"authentik_stages_authenticator_webauthn:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}"
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23user"
|
||||
class="ak-root-link pf-c-button pf-m-primary">{% trans "Configure WebAuthn" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeTypes
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
|
@ -48,11 +49,18 @@ class PasswordStage(ConfigurableStage, Stage):
|
|||
return PasswordStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
def ui_user_settings(self) -> Optional[Challenge]:
|
||||
if not self.configure_flow:
|
||||
return None
|
||||
return reverse(
|
||||
"authentik_stages_password:user-settings", kwargs={"stage_uuid": self.pk}
|
||||
return Challenge(
|
||||
data={
|
||||
"type": ChallengeTypes.shell.value,
|
||||
"title": self._meta.verbose_name,
|
||||
"component": reverse(
|
||||
"authentik_stages_password:user-settings",
|
||||
kwargs={"stage_uuid": self.pk},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{% trans 'Reset your password' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<a class="pf-c-button pf-m-primary ak-root-link" href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}">
|
||||
<a class="pf-c-button pf-m-primary ak-root-link" href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23user">
|
||||
{% trans 'Change password' %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
42
swagger.yaml
42
swagger.yaml
|
@ -7326,6 +7326,48 @@ paths:
|
|||
tags:
|
||||
- stages
|
||||
parameters: []
|
||||
/stages/all/user_settings/:
|
||||
get:
|
||||
operationId: stages_all_user_settings
|
||||
description: Get all stages the user can configure
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
required: false
|
||||
type: string
|
||||
- name: search
|
||||
in: query
|
||||
description: A search term.
|
||||
required: false
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
description: Page Index
|
||||
required: false
|
||||
type: integer
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Page Size
|
||||
required: false
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Challenge that gets sent to the client based on which stage
|
||||
is currently active
|
||||
schema:
|
||||
description: ''
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Challenge'
|
||||
tags:
|
||||
- stages
|
||||
parameters: []
|
||||
/stages/all/{stage_uuid}/:
|
||||
get:
|
||||
operationId: stages_all_read
|
||||
|
|
|
@ -97,7 +97,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||
wait = WebDriverWait(interface_admin, self.wait_timeout)
|
||||
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
||||
self.driver.get(self.shell_url("authentik_core:user-settings"))
|
||||
self.driver.get(self.shell_url("authentik_core:user-details"))
|
||||
|
||||
user = User.objects.get(username="foo")
|
||||
self.assertEqual(user.username, "foo")
|
||||
|
@ -196,7 +196,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||
)
|
||||
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
||||
self.driver.get(self.shell_url("authentik_core:user-settings"))
|
||||
self.driver.get(self.shell_url("authentik_core:user-details"))
|
||||
|
||||
self.assert_user(User.objects.get(username="foo"))
|
||||
|
||||
|
|
Reference in New Issue