stages/otp_time: implement configure_flow

This commit is contained in:
Jens Langhammer 2020-09-25 12:56:27 +02:00
parent e66424cc49
commit 3d4c5b8f4e
7 changed files with 74 additions and 10 deletions

View File

@ -11,7 +11,7 @@ class OTPTimeStageSerializer(ModelSerializer):
class Meta: class Meta:
model = OTPTimeStage model = OTPTimeStage
fields = ["pk", "name", "digits"] fields = ["pk", "name", "configure_flow", "digits"]
class OTPTimeStageViewSet(ModelViewSet): class OTPTimeStageViewSet(ModelViewSet):

View File

@ -57,7 +57,7 @@ class OTPTimeStageForm(forms.ModelForm):
class Meta: class Meta:
model = OTPTimeStage model = OTPTimeStage
fields = ["name", "digits"] fields = ["name", "configure_flow", "digits"]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),

View File

@ -0,0 +1,53 @@
# Generated by Django 3.1.1 on 2020-09-25 10:39
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from passbook.stages.otp_time.models import TOTPDigits
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("passbook_flows", "Flow")
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
OTPTimeStage = apps.get_model("passbook_stages_otp_time", "OTPTimeStage")
db_alias = schema_editor.connection.alias
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-otp-time-configure",
designation=FlowDesignation.STAGE_SETUP,
defaults={"name": "Setup Two-Factor authentication"},
)
stage = OTPTimeStage.objects.using(db_alias).update_or_create(
name="default-otp-time-configure", defaults={"digits": TOTPDigits.SIX}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=stage, defaults={"order": 0}
)
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0013_auto_20200924_1605"),
("passbook_stages_otp_time", "0002_auto_20200701_1900"),
]
operations = [
migrations.AddField(
model_name="otptimestage",
name="configure_flow",
field=models.ForeignKey(
blank=True,
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="passbook_flows.flow",
),
),
]

View File

@ -9,7 +9,7 @@ from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from passbook.core.types import UIUserSettings from passbook.core.types import UIUserSettings
from passbook.flows.models import Stage from passbook.flows.models import ConfigurableStage, Stage
class TOTPDigits(models.IntegerChoices): class TOTPDigits(models.IntegerChoices):
@ -19,7 +19,7 @@ class TOTPDigits(models.IntegerChoices):
EIGHT = 8, _("8 digits, not compatible with apps like Google Authenticator") EIGHT = 8, _("8 digits, not compatible with apps like Google Authenticator")
class OTPTimeStage(Stage): class OTPTimeStage(ConfigurableStage, Stage):
"""Enroll a user's device into Time-based OTP.""" """Enroll a user's device into Time-based OTP."""
digits = models.IntegerField(choices=TOTPDigits.choices) digits = models.IntegerField(choices=TOTPDigits.choices)
@ -44,7 +44,10 @@ class OTPTimeStage(Stage):
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[UIUserSettings]:
return UIUserSettings( return UIUserSettings(
name="Time-based OTP", name="Time-based OTP",
url=reverse("passbook_stages_otp_time:user-settings"), url=reverse(
"passbook_stages_otp_time:user-settings",
kwargs={"stage_uuid": self.stage_uuid},
),
) )
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -21,9 +21,11 @@
</p> </p>
<p> <p>
{% if not state %} {% if not state %}
<a href="{% url 'passbook_stages_otp_time:otp-enable' %}" class="pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a> {% if stage.configure_flow %}
<a href="{% url 'passbook-flows:configure' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
{% endif %}
{% else %} {% else %}
<a href="{% url 'passbook_stages_otp_time:disable' %}" class="pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a> <a href="{% url 'passbook_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
{% endif %} {% endif %}
</p> </p>
</div> </div>

View File

@ -4,6 +4,8 @@ from django.urls import path
from passbook.stages.otp_time.views import DisableView, UserSettingsView from passbook.stages.otp_time.views import DisableView, UserSettingsView
urlpatterns = [ urlpatterns = [
path("settings", UserSettingsView.as_view(), name="user-settings"), path(
path("disable", DisableView.as_view(), name="disable"), "<uuid:stage_uuid>/settings/", UserSettingsView.as_view(), name="user-settings"
),
path("<uuid:stage_uuid>/disable/", DisableView.as_view(), name="disable"),
] ]

View File

@ -2,12 +2,13 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect from django.shortcuts import get_object_or_404, redirect
from django.views import View from django.views import View
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from passbook.audit.models import Event from passbook.audit.models import Event
from passbook.stages.otp_time.models import OTPTimeStage
class UserSettingsView(LoginRequiredMixin, TemplateView): class UserSettingsView(LoginRequiredMixin, TemplateView):
@ -18,6 +19,9 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
# TODO: Check if OTP Stage exists and applies to user # TODO: Check if OTP Stage exists and applies to user
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
stage = get_object_or_404(OTPTimeStage, pk=self.kwargs["stage_uuid"])
kwargs["stage"] = stage
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True) totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
kwargs["state"] = totp_devices.exists() kwargs["state"] = totp_devices.exists()
return kwargs return kwargs