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"
|
SKIP = "skip"
|
||||||
DENY = "deny"
|
DENY = "deny"
|
||||||
# CONFIGURE = "configure"
|
CONFIGURE = "configure"
|
||||||
|
|
||||||
|
|
||||||
class FlowDesignation(models.TextChoices):
|
class FlowDesignation(models.TextChoices):
|
||||||
|
|
|
@ -47,6 +47,11 @@ class FlowPlan:
|
||||||
self.stages.append(stage)
|
self.stages.append(stage)
|
||||||
self.markers.append(marker or StageMarker())
|
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]:
|
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]:
|
||||||
"""Return next pending stage from the bottom of the list"""
|
"""Return next pending stage from the bottom of the list"""
|
||||||
if not self.has_stages:
|
if not self.has_stages:
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
"""AuthenticatorValidateStage API Views"""
|
"""AuthenticatorValidateStage API Views"""
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.flows.api.stages import StageSerializer
|
from authentik.flows.api.stages import StageSerializer
|
||||||
|
from authentik.flows.models import NotConfiguredAction
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorValidateStageSerializer(StageSerializer):
|
class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||||
"""AuthenticatorValidateStage Serializer"""
|
"""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:
|
class Meta:
|
||||||
|
|
||||||
model = AuthenticatorValidateStage
|
model = AuthenticatorValidateStage
|
||||||
fields = StageSerializer.Meta.fields + [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"not_configured_action",
|
"not_configured_action",
|
||||||
"device_classes",
|
"device_classes",
|
||||||
|
"configuration_stage",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp import match_token
|
from django_otp import match_token
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.flows.models import NotConfiguredAction
|
||||||
from authentik.stages.authenticator_validate.models import (
|
from authentik.stages.authenticator_validate.models import (
|
||||||
AuthenticatorValidateStage,
|
AuthenticatorValidateStage,
|
||||||
DeviceClasses,
|
DeviceClasses,
|
||||||
|
@ -42,10 +43,31 @@ class ValidationForm(forms.Form):
|
||||||
class AuthenticatorValidateStageForm(forms.ModelForm):
|
class AuthenticatorValidateStageForm(forms.ModelForm):
|
||||||
"""OTP Validate stage forms"""
|
"""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:
|
class Meta:
|
||||||
|
|
||||||
model = AuthenticatorValidateStage
|
model = AuthenticatorValidateStage
|
||||||
fields = ["name", "device_classes"]
|
fields = [
|
||||||
|
"name",
|
||||||
|
"not_configured_action",
|
||||||
|
"device_classes",
|
||||||
|
"configuration_stage",
|
||||||
|
]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"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
|
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(
|
device_classes = ArrayField(
|
||||||
models.TextField(),
|
models.TextField(),
|
||||||
help_text=_("Device classes which can be used to authenticate"),
|
help_text=_("Device classes which can be used to authenticate"),
|
||||||
|
|
|
@ -133,6 +133,12 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||||
if stage.not_configured_action == NotConfiguredAction.DENY:
|
if stage.not_configured_action == NotConfiguredAction.DENY:
|
||||||
LOGGER.debug("Authenticator not configured, denying")
|
LOGGER.debug("Authenticator not configured, denying")
|
||||||
return self.executor.stage_invalid()
|
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)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_challenge(self) -> AuthenticatorChallenge:
|
def get_challenge(self) -> AuthenticatorChallenge:
|
||||||
|
|
|
@ -11070,6 +11070,7 @@ definitions:
|
||||||
enum:
|
enum:
|
||||||
- skip
|
- skip
|
||||||
- deny
|
- deny
|
||||||
|
- configure
|
||||||
device_classes:
|
device_classes:
|
||||||
description: ''
|
description: ''
|
||||||
type: array
|
type: array
|
||||||
|
@ -11077,6 +11078,14 @@ definitions:
|
||||||
title: Device classes
|
title: Device classes
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
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:
|
AuthenticateWebAuthnStage:
|
||||||
description: AuthenticateWebAuthnStage Serializer
|
description: AuthenticateWebAuthnStage Serializer
|
||||||
required:
|
required:
|
||||||
|
|
Reference in New Issue