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