stages/authenticator_validate: add configuration stage to configure Authenticator
This commit is contained in:
parent
644a03e40e
commit
ed8b78600e
|
@ -24,7 +24,7 @@ class NotConfiguredAction(models.TextChoices):
|
|||
|
||||
SKIP = "skip"
|
||||
DENY = "deny"
|
||||
# CONFIGURE = "configure"
|
||||
CONFIGURE = "configure"
|
||||
|
||||
|
||||
class FlowDesignation(models.TextChoices):
|
||||
|
|
|
@ -47,6 +47,11 @@ class FlowPlan:
|
|||
self.stages.append(stage)
|
||||
self.markers.append(marker or StageMarker())
|
||||
|
||||
def insert(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||
"""Insert stage into plan, as immediate next stage"""
|
||||
self.stages.insert(1, stage)
|
||||
self.markers.insert(1, marker or StageMarker())
|
||||
|
||||
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]:
|
||||
"""Return next pending stage from the bottom of the list"""
|
||||
if not self.has_stages:
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
"""AuthenticatorValidateStage API Views"""
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
|
||||
|
||||
class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||
"""AuthenticatorValidateStage Serializer"""
|
||||
|
||||
def validate_not_configured_action(self, value):
|
||||
"""Ensure that a configuration stage is set when not_configured_action is configure"""
|
||||
configuration_stage = self.initial_data.get("configuration_stage")
|
||||
if value == NotConfiguredAction.CONFIGURE and configuration_stage is None:
|
||||
raise ValidationError(
|
||||
(
|
||||
'When "Not configured action" is set to "Configure", '
|
||||
"you must set a configuration stage."
|
||||
)
|
||||
)
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticatorValidateStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"not_configured_action",
|
||||
"device_classes",
|
||||
"configuration_stage",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django_otp import match_token
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.stages.authenticator_validate.models import (
|
||||
AuthenticatorValidateStage,
|
||||
DeviceClasses,
|
||||
|
@ -42,10 +43,31 @@ class ValidationForm(forms.Form):
|
|||
class AuthenticatorValidateStageForm(forms.ModelForm):
|
||||
"""OTP Validate stage forms"""
|
||||
|
||||
def clean_not_configured_action(self):
|
||||
"""Ensure that a configuration stage is set when not_configured_action is configure"""
|
||||
not_configured_action = self.cleaned_data.get("not_configured_action")
|
||||
configuration_stage = self.cleaned_data.get("configuration_stage")
|
||||
if (
|
||||
not_configured_action == NotConfiguredAction.CONFIGURE
|
||||
and configuration_stage is None
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
(
|
||||
'When "Not configured action" is set to "Configure", '
|
||||
"you must set a configuration stage."
|
||||
)
|
||||
)
|
||||
return not_configured_action
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticatorValidateStage
|
||||
fields = ["name", "device_classes"]
|
||||
fields = [
|
||||
"name",
|
||||
"not_configured_action",
|
||||
"device_classes",
|
||||
"configuration_stage",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.1.7 on 2021-03-01 17:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
("authentik_stages_authenticator_validate", "0004_auto_20210301_0949"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="configuration_stage",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
related_name="+",
|
||||
to="authentik_flows.stage",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.1.7 on 2021-03-01 17:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_validate",
|
||||
"0005_authenticatorvalidatestage_configuration_stage",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="not_configured_action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("skip", "Skip"),
|
||||
("deny", "Deny"),
|
||||
("configure", "Configure"),
|
||||
],
|
||||
default="skip",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -36,6 +36,21 @@ class AuthenticatorValidateStage(Stage):
|
|||
choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
|
||||
)
|
||||
|
||||
configuration_stage = models.ForeignKey(
|
||||
Stage,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="+",
|
||||
help_text=_(
|
||||
(
|
||||
"Stage used to configure Authenticator when user doesn't have any compatible "
|
||||
"devices. After this configuration Stage passes, the user is not prompted again."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
device_classes = ArrayField(
|
||||
models.TextField(),
|
||||
help_text=_("Device classes which can be used to authenticate"),
|
||||
|
|
|
@ -133,6 +133,12 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
if stage.not_configured_action == NotConfiguredAction.DENY:
|
||||
LOGGER.debug("Authenticator not configured, denying")
|
||||
return self.executor.stage_invalid()
|
||||
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
|
||||
LOGGER.debug("Authenticator not configured, sending user to configure")
|
||||
# plan.insert inserts at 1 index, so when stage_ok pops 0,
|
||||
# the configuration stage is next
|
||||
self.executor.plan.insert(stage.configuration_stage)
|
||||
return self.executor.stage_ok()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_challenge(self) -> AuthenticatorChallenge:
|
||||
|
|
|
@ -11070,6 +11070,7 @@ definitions:
|
|||
enum:
|
||||
- skip
|
||||
- deny
|
||||
- configure
|
||||
device_classes:
|
||||
description: ''
|
||||
type: array
|
||||
|
@ -11077,6 +11078,14 @@ definitions:
|
|||
title: Device classes
|
||||
type: string
|
||||
minLength: 1
|
||||
configuration_stage:
|
||||
title: Configuration stage
|
||||
description: Stage used to configure Authenticator when user doesn't have
|
||||
any compatible devices. After this configuration Stage passes, the user
|
||||
is not prompted again.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
AuthenticateWebAuthnStage:
|
||||
description: AuthenticateWebAuthnStage Serializer
|
||||
required:
|
||||
|
|
Reference in New Issue