From 3d4c5b8f4e3eda4e15d52d6f773ed9999c5d403a Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Fri, 25 Sep 2020 12:56:27 +0200
Subject: [PATCH] stages/otp_time: implement configure_flow
---
passbook/stages/otp_time/api.py | 2 +-
passbook/stages/otp_time/forms.py | 2 +-
.../0003_otptimestage_configure_flow.py | 53 +++++++++++++++++++
passbook/stages/otp_time/models.py | 9 ++--
.../stages/otp_time/user_settings.html | 6 ++-
passbook/stages/otp_time/urls.py | 6 ++-
passbook/stages/otp_time/views.py | 6 ++-
7 files changed, 74 insertions(+), 10 deletions(-)
create mode 100644 passbook/stages/otp_time/migrations/0003_otptimestage_configure_flow.py
diff --git a/passbook/stages/otp_time/api.py b/passbook/stages/otp_time/api.py
index 3ce955c01..f7998b3d7 100644
--- a/passbook/stages/otp_time/api.py
+++ b/passbook/stages/otp_time/api.py
@@ -11,7 +11,7 @@ class OTPTimeStageSerializer(ModelSerializer):
class Meta:
model = OTPTimeStage
- fields = ["pk", "name", "digits"]
+ fields = ["pk", "name", "configure_flow", "digits"]
class OTPTimeStageViewSet(ModelViewSet):
diff --git a/passbook/stages/otp_time/forms.py b/passbook/stages/otp_time/forms.py
index 36052404d..38ce710fb 100644
--- a/passbook/stages/otp_time/forms.py
+++ b/passbook/stages/otp_time/forms.py
@@ -57,7 +57,7 @@ class OTPTimeStageForm(forms.ModelForm):
class Meta:
model = OTPTimeStage
- fields = ["name", "digits"]
+ fields = ["name", "configure_flow", "digits"]
widgets = {
"name": forms.TextInput(),
diff --git a/passbook/stages/otp_time/migrations/0003_otptimestage_configure_flow.py b/passbook/stages/otp_time/migrations/0003_otptimestage_configure_flow.py
new file mode 100644
index 000000000..7182ea157
--- /dev/null
+++ b/passbook/stages/otp_time/migrations/0003_otptimestage_configure_flow.py
@@ -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",
+ ),
+ ),
+ ]
diff --git a/passbook/stages/otp_time/models.py b/passbook/stages/otp_time/models.py
index 130c3d79e..e82b33936 100644
--- a/passbook/stages/otp_time/models.py
+++ b/passbook/stages/otp_time/models.py
@@ -9,7 +9,7 @@ from django.views import View
from rest_framework.serializers import BaseSerializer
from passbook.core.types import UIUserSettings
-from passbook.flows.models import Stage
+from passbook.flows.models import ConfigurableStage, Stage
class TOTPDigits(models.IntegerChoices):
@@ -19,7 +19,7 @@ class TOTPDigits(models.IntegerChoices):
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."""
digits = models.IntegerField(choices=TOTPDigits.choices)
@@ -44,7 +44,10 @@ class OTPTimeStage(Stage):
def ui_user_settings(self) -> Optional[UIUserSettings]:
return UIUserSettings(
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:
diff --git a/passbook/stages/otp_time/templates/stages/otp_time/user_settings.html b/passbook/stages/otp_time/templates/stages/otp_time/user_settings.html
index 98f13e177..71789eee2 100644
--- a/passbook/stages/otp_time/templates/stages/otp_time/user_settings.html
+++ b/passbook/stages/otp_time/templates/stages/otp_time/user_settings.html
@@ -21,9 +21,11 @@
{% if not state %}
- {% trans "Enable Time-based OTP" %}
+ {% if stage.configure_flow %}
+ {% trans "Enable Time-based OTP" %}
+ {% endif %}
{% else %}
- {% trans "Disable Time-based OTP" %}
+ {% trans "Disable Time-based OTP" %}
{% endif %}
diff --git a/passbook/stages/otp_time/urls.py b/passbook/stages/otp_time/urls.py
index 6aa07bc5a..a4a81ac93 100644
--- a/passbook/stages/otp_time/urls.py
+++ b/passbook/stages/otp_time/urls.py
@@ -4,6 +4,8 @@ from django.urls import path
from passbook.stages.otp_time.views import DisableView, UserSettingsView
urlpatterns = [
- path("settings", UserSettingsView.as_view(), name="user-settings"),
- path("disable", DisableView.as_view(), name="disable"),
+ path(
+ "/settings/", UserSettingsView.as_view(), name="user-settings"
+ ),
+ path("/disable/", DisableView.as_view(), name="disable"),
]
diff --git a/passbook/stages/otp_time/views.py b/passbook/stages/otp_time/views.py
index 41534e509..dcf4b990d 100644
--- a/passbook/stages/otp_time/views.py
+++ b/passbook/stages/otp_time/views.py
@@ -2,12 +2,13 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
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.generic import TemplateView
from django_otp.plugins.otp_totp.models import TOTPDevice
from passbook.audit.models import Event
+from passbook.stages.otp_time.models import OTPTimeStage
class UserSettingsView(LoginRequiredMixin, TemplateView):
@@ -18,6 +19,9 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
# TODO: Check if OTP Stage exists and applies to user
def get_context_data(self, **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)
kwargs["state"] = totp_devices.exists()
return kwargs