From 212e966dd403767a2fd0a7f024e9b88a24e24d97 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Fri, 8 May 2020 19:46:39 +0200
Subject: [PATCH] factors: -> stage
---
passbook/admin/urls.py | 22 +++----
passbook/admin/views/overview.py | 14 ++---
.../admin/views/{factors.py => stages.py} | 62 +++++++++----------
passbook/api/v2/urls.py | 27 ++++----
passbook/core/api/factors.py | 30 ---------
.../core/migrations/0012_delete_factor.py | 14 +++++
passbook/core/models.py | 24 -------
passbook/core/templates/user/base.html | 12 ++--
.../templatetags/passbook_user_settings.py | 44 ++++++-------
passbook/core/types.py | 2 +-
passbook/core/views/authentication.py | 2 +-
passbook/core/views/user.py | 2 +-
passbook/factors/captcha/api.py | 21 -------
passbook/factors/captcha/apps.py | 10 ---
passbook/factors/captcha/forms.py | 35 -----------
.../captcha/migrations/0001_initial.py | 39 ------------
.../migrations/0002_auto_20200221_1410.py | 27 --------
passbook/factors/dummy/api.py | 21 -------
passbook/factors/dummy/apps.py | 11 ----
passbook/factors/dummy/factor.py | 12 ----
passbook/factors/dummy/forms.py | 21 -------
passbook/factors/dummy/models.py | 19 ------
passbook/factors/email/apps.py | 15 -----
.../migrations/0002_auto_20191011_1224.py | 18 ------
passbook/factors/otp/api.py | 21 -------
passbook/factors/otp/apps.py | 12 ----
passbook/factors/otp/models.py | 34 ----------
passbook/factors/password/api.py | 30 ---------
passbook/factors/password/apps.py | 15 -----
.../migrations/0002_auto_20191007_1411.py | 24 -------
.../0003_passwordfactor_reset_factors.py | 21 -------
.../migrations/0004_auto_20200221_1410.py | 23 -------
passbook/factors/password/signals.py | 23 -------
passbook/flows/api.py | 49 +++++++++++----
passbook/flows/forms.py | 21 +++----
passbook/flows/migrations/0001_initial.py | 54 +++++++++++-----
...default_flows.py => 0002_default_flows.py} | 18 ++++--
..._flowfactorbinding_re_evaluate_policies.py | 21 -------
.../migrations/0003_auto_20200508_1230.py | 21 -------
.../migrations/0005_auto_20200508_1642.py | 23 -------
passbook/flows/models.py | 46 ++++++++++----
passbook/flows/planner.py | 31 ++++++----
passbook/flows/{factor_base.py => stage.py} | 6 +-
passbook/flows/views.py | 61 +++++++++---------
passbook/root/settings.py | 10 +--
passbook/sources/oauth/views/core.py | 4 +-
passbook/{factors => stages}/__init__.py | 0
.../{factors => stages}/captcha/__init__.py | 0
passbook/stages/captcha/api.py | 21 +++++++
passbook/stages/captcha/apps.py | 10 +++
passbook/stages/captcha/forms.py | 25 ++++++++
.../stages/captcha/migrations/0001_initial.py | 49 +++++++++++++++
.../captcha/migrations/__init__.py | 0
.../{factors => stages}/captcha/models.py | 18 +++---
.../{factors => stages}/captcha/settings.py | 2 +-
.../factor.py => stages/captcha/stage.py} | 14 ++---
.../{factors => stages}/dummy/__init__.py | 0
passbook/stages/dummy/api.py | 21 +++++++
passbook/stages/dummy/apps.py | 11 ++++
passbook/stages/dummy/forms.py | 16 +++++
.../dummy/migrations/0001_initial.py | 16 ++---
.../dummy/migrations/__init__.py | 0
passbook/stages/dummy/models.py | 19 ++++++
passbook/stages/dummy/stage.py | 12 ++++
.../{factors => stages}/email/__init__.py | 0
passbook/{factors => stages}/email/api.py | 21 +++----
passbook/stages/email/apps.py | 15 +++++
passbook/{factors => stages}/email/forms.py | 20 ++----
.../email/migrations/0001_initial.py | 18 +++---
.../email/migrations/__init__.py | 0
passbook/{factors => stages}/email/models.py | 18 +++---
.../email/factor.py => stages/email/stage.py} | 18 +++---
passbook/{factors => stages}/email/tasks.py | 20 +++---
passbook/{factors => stages}/email/utils.py | 0
passbook/{factors => stages}/otp/__init__.py | 0
passbook/stages/otp/api.py | 21 +++++++
passbook/stages/otp/apps.py | 12 ++++
passbook/{factors => stages}/otp/forms.py | 19 ++----
.../otp/migrations/0001_initial.py | 19 +++---
.../otp/migrations/__init__.py | 0
passbook/stages/otp/models.py | 34 ++++++++++
passbook/{factors => stages}/otp/settings.py | 0
.../otp/factors.py => stages/otp/stage.py} | 18 +++---
.../otp/templates/stages}/otp/factor.html | 0
.../templates/stages}/otp/user_settings.html | 4 +-
passbook/{factors => stages}/otp/urls.py | 2 +-
passbook/{factors => stages}/otp/utils.py | 0
passbook/{factors => stages}/otp/views.py | 20 +++---
.../{factors => stages}/password/__init__.py | 0
passbook/stages/password/api.py | 26 ++++++++
passbook/stages/password/apps.py | 10 +++
.../password/exceptions.py | 0
.../{factors => stages}/password/forms.py | 19 ++----
.../password/migrations/0001_initial.py | 21 ++++---
.../password/migrations/__init__.py | 0
.../{factors => stages}/password/models.py | 22 +++----
.../factor.py => stages/password/stage.py} | 18 +++---
.../templates/stages}/password/backend.html | 0
scripts/coverage.sh | 2 +-
99 files changed, 745 insertions(+), 958 deletions(-)
rename passbook/admin/views/{factors.py => stages.py} (62%)
delete mode 100644 passbook/core/api/factors.py
create mode 100644 passbook/core/migrations/0012_delete_factor.py
delete mode 100644 passbook/factors/captcha/api.py
delete mode 100644 passbook/factors/captcha/apps.py
delete mode 100644 passbook/factors/captcha/forms.py
delete mode 100644 passbook/factors/captcha/migrations/0001_initial.py
delete mode 100644 passbook/factors/captcha/migrations/0002_auto_20200221_1410.py
delete mode 100644 passbook/factors/dummy/api.py
delete mode 100644 passbook/factors/dummy/apps.py
delete mode 100644 passbook/factors/dummy/factor.py
delete mode 100644 passbook/factors/dummy/forms.py
delete mode 100644 passbook/factors/dummy/models.py
delete mode 100644 passbook/factors/email/apps.py
delete mode 100644 passbook/factors/email/migrations/0002_auto_20191011_1224.py
delete mode 100644 passbook/factors/otp/api.py
delete mode 100644 passbook/factors/otp/apps.py
delete mode 100644 passbook/factors/otp/models.py
delete mode 100644 passbook/factors/password/api.py
delete mode 100644 passbook/factors/password/apps.py
delete mode 100644 passbook/factors/password/migrations/0002_auto_20191007_1411.py
delete mode 100644 passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py
delete mode 100644 passbook/factors/password/migrations/0004_auto_20200221_1410.py
delete mode 100644 passbook/factors/password/signals.py
rename passbook/flows/migrations/{0004_default_flows.py => 0002_default_flows.py} (58%)
delete mode 100644 passbook/flows/migrations/0002_flowfactorbinding_re_evaluate_policies.py
delete mode 100644 passbook/flows/migrations/0003_auto_20200508_1230.py
delete mode 100644 passbook/flows/migrations/0005_auto_20200508_1642.py
rename passbook/flows/{factor_base.py => stage.py} (84%)
rename passbook/{factors => stages}/__init__.py (100%)
rename passbook/{factors => stages}/captcha/__init__.py (100%)
create mode 100644 passbook/stages/captcha/api.py
create mode 100644 passbook/stages/captcha/apps.py
create mode 100644 passbook/stages/captcha/forms.py
create mode 100644 passbook/stages/captcha/migrations/0001_initial.py
rename passbook/{factors => stages}/captcha/migrations/__init__.py (100%)
rename passbook/{factors => stages}/captcha/models.py (53%)
rename passbook/{factors => stages}/captcha/settings.py (90%)
rename passbook/{factors/captcha/factor.py => stages/captcha/stage.py} (66%)
rename passbook/{factors => stages}/dummy/__init__.py (100%)
create mode 100644 passbook/stages/dummy/api.py
create mode 100644 passbook/stages/dummy/apps.py
create mode 100644 passbook/stages/dummy/forms.py
rename passbook/{factors => stages}/dummy/migrations/0001_initial.py (64%)
rename passbook/{factors => stages}/dummy/migrations/__init__.py (100%)
create mode 100644 passbook/stages/dummy/models.py
create mode 100644 passbook/stages/dummy/stage.py
rename passbook/{factors => stages}/email/__init__.py (100%)
rename passbook/{factors => stages}/email/api.py (54%)
create mode 100644 passbook/stages/email/apps.py
rename passbook/{factors => stages}/email/forms.py (60%)
rename passbook/{factors => stages}/email/migrations/0001_initial.py (76%)
rename passbook/{factors => stages}/email/migrations/__init__.py (100%)
rename passbook/{factors => stages}/email/models.py (76%)
rename passbook/{factors/email/factor.py => stages/email/stage.py} (75%)
rename passbook/{factors => stages}/email/tasks.py (62%)
rename passbook/{factors => stages}/email/utils.py (100%)
rename passbook/{factors => stages}/otp/__init__.py (100%)
create mode 100644 passbook/stages/otp/api.py
create mode 100644 passbook/stages/otp/apps.py
rename passbook/{factors => stages}/otp/forms.py (77%)
rename passbook/{factors => stages}/otp/migrations/0001_initial.py (67%)
rename passbook/{factors => stages}/otp/migrations/__init__.py (100%)
create mode 100644 passbook/stages/otp/models.py
rename passbook/{factors => stages}/otp/settings.py (100%)
rename passbook/{factors/otp/factors.py => stages/otp/stage.py} (82%)
rename passbook/{factors/otp/templates => stages/otp/templates/stages}/otp/factor.html (100%)
rename passbook/{factors/otp/templates => stages/otp/templates/stages}/otp/user_settings.html (91%)
rename passbook/{factors => stages}/otp/urls.py (89%)
rename passbook/{factors => stages}/otp/utils.py (100%)
rename passbook/{factors => stages}/otp/views.py (91%)
rename passbook/{factors => stages}/password/__init__.py (100%)
create mode 100644 passbook/stages/password/api.py
create mode 100644 passbook/stages/password/apps.py
rename passbook/{factors => stages}/password/exceptions.py (100%)
rename passbook/{factors => stages}/password/forms.py (64%)
rename passbook/{factors => stages}/password/migrations/0001_initial.py (62%)
rename passbook/{factors => stages}/password/migrations/__init__.py (100%)
rename passbook/{factors => stages}/password/models.py (63%)
rename passbook/{factors/password/factor.py => stages/password/stage.py} (86%)
rename passbook/{factors/password/templates/factors => stages/password/templates/stages}/password/backend.html (100%)
diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py
index 6435a446d..0f3526713 100644
--- a/passbook/admin/urls.py
+++ b/passbook/admin/urls.py
@@ -6,7 +6,6 @@ from passbook.admin.views import (
audit,
certificate_key_pair,
debug,
- factors,
flows,
groups,
invitations,
@@ -15,6 +14,7 @@ from passbook.admin.views import (
property_mapping,
providers,
sources,
+ stages,
users,
)
@@ -85,18 +85,18 @@ urlpatterns = [
providers.ProviderDeleteView.as_view(),
name="provider-delete",
),
- # Factors
- path("factors/", factors.FactorListView.as_view(), name="factors"),
- path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"),
+ # Stages
+ path("stages/", stages.StageListView.as_view(), name="stages"),
+ path("stages/create/", stages.StageCreateView.as_view(), name="stage-create"),
path(
- "factors//update/",
- factors.FactorUpdateView.as_view(),
- name="factor-update",
+ "stages//update/",
+ stages.StageUpdateView.as_view(),
+ name="stage-update",
),
path(
- "factors//delete/",
- factors.FactorDeleteView.as_view(),
- name="factor-delete",
+ "stages//delete/",
+ stages.StageDeleteView.as_view(),
+ name="stage-delete",
),
# Flows
path("flows/", flows.FlowListView.as_view(), name="flows"),
@@ -107,7 +107,7 @@ urlpatterns = [
path(
"flows//delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
),
- # Factors
+ # Property Mappings
path(
"property-mappings/",
property_mapping.PropertyMappingListView.as_view(),
diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py
index 52025b50e..9248eac5b 100644
--- a/passbook/admin/views/overview.py
+++ b/passbook/admin/views/overview.py
@@ -5,15 +5,8 @@ from django.views.generic import TemplateView
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
-from passbook.core.models import (
- Application,
- Factor,
- Invitation,
- Policy,
- Provider,
- Source,
- User,
-)
+from passbook.core.models import Application, Invitation, Policy, Provider, Source, User
+from passbook.flows.models import Flow, Stage
from passbook.root.celery import CELERY_APP
@@ -35,7 +28,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["user_count"] = len(User.objects.all())
kwargs["provider_count"] = len(Provider.objects.all())
kwargs["source_count"] = len(Source.objects.all())
- kwargs["factor_count"] = len(Factor.objects.all())
+ kwargs["stage_count"] = len(Stage.objects.all())
+ kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = __version__
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
diff --git a/passbook/admin/views/factors.py b/passbook/admin/views/stages.py
similarity index 62%
rename from passbook/admin/views/factors.py
rename to passbook/admin/views/stages.py
index 628c6a61f..c5623a22e 100644
--- a/passbook/admin/views/factors.py
+++ b/passbook/admin/views/stages.py
@@ -1,4 +1,4 @@
-"""passbook Factor administration"""
+"""passbook Stage administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
-from passbook.core.models import Factor
+from passbook.flows.models import Stage
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
@@ -23,18 +23,18 @@ def all_subclasses(cls):
)
-class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
- """Show list of all factors"""
+class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
+ """Show list of all flows"""
- model = Factor
- template_name = "administration/factor/list.html"
- permission_required = "passbook_core.view_factor"
+ model = Stage
+ template_name = "administration/flow/list.html"
+ permission_required = "passbook_core.view_flow"
ordering = "order"
paginate_by = 40
def get_context_data(self, **kwargs):
kwargs["types"] = {
- x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)
+ x.__name__: x._meta.verbose_name for x in all_subclasses(Stage)
}
return super().get_context_data(**kwargs)
@@ -42,46 +42,46 @@ class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
return super().get_queryset().select_subclasses()
-class FactorCreateView(
+class StageCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
- """Create new Factor"""
+ """Create new Stage"""
- model = Factor
+ model = Stage
template_name = "generic/create.html"
- permission_required = "passbook_core.add_factor"
+ permission_required = "passbook_core.add_flow"
- success_url = reverse_lazy("passbook_admin:factors")
- success_message = _("Successfully created Factor")
+ success_url = reverse_lazy("passbook_admin:flows")
+ success_message = _("Successfully created Stage")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
- factor_type = self.request.GET.get("type")
- model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
+ flow_type = self.request.GET.get("type")
+ model = next(x for x in all_subclasses(Stage) if x.__name__ == flow_type)
kwargs["type"] = model._meta.verbose_name
return kwargs
def get_form_class(self):
- factor_type = self.request.GET.get("type")
- model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
+ flow_type = self.request.GET.get("type")
+ model = next(x for x in all_subclasses(Stage) if x.__name__ == flow_type)
if not model:
raise Http404
return path_to_class(model.form)
-class FactorUpdateView(
+class StageUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
- """Update factor"""
+ """Update flow"""
- model = Factor
+ model = Stage
permission_required = "passbook_core.update_application"
template_name = "generic/update.html"
- success_url = reverse_lazy("passbook_admin:factors")
- success_message = _("Successfully updated Factor")
+ success_url = reverse_lazy("passbook_admin:flows")
+ success_message = _("Successfully updated Stage")
def get_form_class(self):
form_class_path = self.get_object().form
@@ -90,24 +90,24 @@ class FactorUpdateView(
def get_object(self, queryset=None):
return (
- Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
+ Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
-class FactorDeleteView(
+class StageDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
- """Delete factor"""
+ """Delete flow"""
- model = Factor
+ model = Stage
template_name = "generic/delete.html"
- permission_required = "passbook_core.delete_factor"
- success_url = reverse_lazy("passbook_admin:factors")
- success_message = _("Successfully deleted Factor")
+ permission_required = "passbook_core.delete_flow"
+ success_url = reverse_lazy("passbook_admin:flows")
+ success_message = _("Successfully deleted Stage")
def get_object(self, queryset=None):
return (
- Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
+ Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs):
diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py
index 524de960e..d651e991f 100644
--- a/passbook/api/v2/urls.py
+++ b/passbook/api/v2/urls.py
@@ -9,7 +9,6 @@ from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api import EventViewSet
from passbook.core.api.applications import ApplicationViewSet
-from passbook.core.api.factors import FactorViewSet
from passbook.core.api.groups import GroupViewSet
from passbook.core.api.invitations import InvitationViewSet
from passbook.core.api.policies import PolicyViewSet
@@ -17,12 +16,7 @@ from passbook.core.api.propertymappings import PropertyMappingViewSet
from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet
from passbook.core.api.users import UserViewSet
-from passbook.factors.captcha.api import CaptchaFactorViewSet
-from passbook.factors.dummy.api import DummyFactorViewSet
-from passbook.factors.email.api import EmailFactorViewSet
-from passbook.factors.otp.api import OTPFactorViewSet
-from passbook.factors.password.api import PasswordFactorViewSet
-from passbook.flows.api import FlowFactorBindingViewSet, FlowViewSet
+from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from passbook.lib.utils.reflection import get_apps
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.expression.api import ExpressionPolicyViewSet
@@ -36,6 +30,11 @@ from passbook.providers.oidc.api import OpenIDProviderViewSet
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from passbook.sources.oauth.api import OAuthSourceViewSet
+from passbook.stages.captcha.api import CaptchaStageViewSet
+from passbook.stages.dummy.api import DummyStageViewSet
+from passbook.stages.email.api import EmailStageViewSet
+from passbook.stages.otp.api import OTPStageViewSet
+from passbook.stages.password.api import PasswordStageViewSet
LOGGER = get_logger()
router = routers.DefaultRouter()
@@ -69,14 +68,14 @@ router.register("providers/saml", SAMLProviderViewSet)
router.register("propertymappings/all", PropertyMappingViewSet)
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
-router.register("factors/all", FactorViewSet)
-router.register("factors/captcha", CaptchaFactorViewSet)
-router.register("factors/dummy", DummyFactorViewSet)
-router.register("factors/email", EmailFactorViewSet)
-router.register("factors/otp", OTPFactorViewSet)
-router.register("factors/password", PasswordFactorViewSet)
+router.register("stages/all", StageViewSet)
+router.register("stages/captcha", CaptchaStageViewSet)
+router.register("stages/dummy", DummyStageViewSet)
+router.register("stages/email", EmailStageViewSet)
+router.register("stages/otp", OTPStageViewSet)
+router.register("stages/password", PasswordStageViewSet)
router.register("flows", FlowViewSet)
-router.register("flows/bindings", FlowFactorBindingViewSet)
+router.register("flows/bindings", FlowStageBindingViewSet)
info = openapi.Info(
title="passbook API",
diff --git a/passbook/core/api/factors.py b/passbook/core/api/factors.py
deleted file mode 100644
index ec812fa8b..000000000
--- a/passbook/core/api/factors.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Factor API Views"""
-from rest_framework.serializers import ModelSerializer, SerializerMethodField
-from rest_framework.viewsets import ReadOnlyModelViewSet
-
-from passbook.core.models import Factor
-
-
-class FactorSerializer(ModelSerializer):
- """Factor Serializer"""
-
- __type__ = SerializerMethodField(method_name="get_type")
-
- def get_type(self, obj):
- """Get object type so that we know which API Endpoint to use to get the full object"""
- return obj._meta.object_name.lower().replace("factor", "")
-
- class Meta:
-
- model = Factor
- fields = ["pk", "name", "slug", "order", "enabled", "__type__"]
-
-
-class FactorViewSet(ReadOnlyModelViewSet):
- """Factor Viewset"""
-
- queryset = Factor.objects.all()
- serializer_class = FactorSerializer
-
- def get_queryset(self):
- return Factor.objects.select_subclasses()
diff --git a/passbook/core/migrations/0012_delete_factor.py b/passbook/core/migrations/0012_delete_factor.py
new file mode 100644
index 000000000..f8f9c3128
--- /dev/null
+++ b/passbook/core/migrations/0012_delete_factor.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.3 on 2020-05-08 17:58
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("passbook_core", "0011_auto_20200222_1822"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(name="Factor",),
+ ]
diff --git a/passbook/core/models.py b/passbook/core/models.py
index 25d33db16..29f489f2a 100644
--- a/passbook/core/models.py
+++ b/passbook/core/models.py
@@ -103,30 +103,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField("Policy", blank=True)
-class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
- """Authentication factor, multiple instances of the same Factor can be used"""
-
- name = models.TextField(help_text=_("Factor's display Name."))
- slug = models.SlugField(
- unique=True, help_text=_("Internal factor name, used in URLs.")
- )
- order = models.IntegerField()
- enabled = models.BooleanField(default=True)
-
- objects = InheritanceManager()
- type = ""
- form = ""
-
- @property
- def ui_user_settings(self) -> Optional[UIUserSettings]:
- """Entrypoint to integrate with User settings. Can either return None if no
- user settings are available, or an instanace of UIUserSettings."""
- return None
-
- def __str__(self):
- return f"Factor {self.slug}"
-
-
class Application(ExportModelOperationsMixin("application"), PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to
diff --git a/passbook/core/templates/user/base.html b/passbook/core/templates/user/base.html
index 6e9d0e650..274d08346 100644
--- a/passbook/core/templates/user/base.html
+++ b/passbook/core/templates/user/base.html
@@ -18,16 +18,16 @@
- {% user_factors as user_factors_loc %}
- {% if user_factors_loc %}
+ {% user_stages as user_stages_loc %}
+ {% if user_stages_loc %}
{% trans 'Factors' %}
- {% for factor in user_factors_loc %}
+ {% for stage in user_stages_loc %}
-
-
-
- {{ factor.name }}
+
+
+ {{ stage.name }}
{% endfor %}
diff --git a/passbook/core/templatetags/passbook_user_settings.py b/passbook/core/templatetags/passbook_user_settings.py
index 478903b1c..57cb25711 100644
--- a/passbook/core/templatetags/passbook_user_settings.py
+++ b/passbook/core/templatetags/passbook_user_settings.py
@@ -4,7 +4,7 @@ from typing import Iterable, List
from django import template
from django.template.context import RequestContext
-from passbook.core.models import Factor, Source
+from passbook.core.models import Source
from passbook.core.types import UIUserSettings
from passbook.policies.engine import PolicyEngine
@@ -12,24 +12,24 @@ register = template.Library()
@register.simple_tag(takes_context=True)
-def user_factors(context: RequestContext) -> List[UIUserSettings]:
- """Return list of all factors which apply to user"""
- user = context.get("request").user
- _all_factors: Iterable[Factor] = (
- Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
- )
- matching_factors: List[UIUserSettings] = []
- for factor in _all_factors:
- user_settings = factor.ui_user_settings
- if not user_settings:
- continue
- policy_engine = PolicyEngine(
- factor.policies.all(), user, context.get("request")
- )
- policy_engine.build()
- if policy_engine.passing:
- matching_factors.append(user_settings)
- return matching_factors
+# pylint: disable=unused-argument
+def user_stages(context: RequestContext) -> List[UIUserSettings]:
+ """Return list of all stages which apply to user"""
+ # TODO: Rewrite this based on flows
+ # user = context.get("request").user
+ # _all_stages: Iterable[Stage] = (Stage.objects.all().select_subclasses())
+ matching_stages: List[UIUserSettings] = []
+ # for stage in _all_stages:
+ # user_settings = stage.ui_user_settings
+ # if not user_settings:
+ # continue
+ # policy_engine = PolicyEngine(
+ # stage.policies.all(), user, context.get("request")
+ # )
+ # policy_engine.build()
+ # if policy_engine.passing:
+ # matching_stages.append(user_settings)
+ return matching_stages
@register.simple_tag(takes_context=True)
@@ -40,12 +40,12 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
Source.objects.filter(enabled=True).select_subclasses()
)
matching_sources: List[UIUserSettings] = []
- for factor in _all_sources:
- user_settings = factor.ui_user_settings
+ for source in _all_sources:
+ user_settings = source.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine(
- factor.policies.all(), user, context.get("request")
+ source.policies.all(), user, context.get("request")
)
policy_engine.build()
if policy_engine.passing:
diff --git a/passbook/core/types.py b/passbook/core/types.py
index 0e03a91af..86d6eeea6 100644
--- a/passbook/core/types.py
+++ b/passbook/core/types.py
@@ -5,7 +5,7 @@ from typing import Optional
@dataclass
class UIUserSettings:
- """Dataclass for Factor and Source's user_settings"""
+ """Dataclass for Stage and Source's user_settings"""
name: str
icon: str
diff --git a/passbook/core/views/authentication.py b/passbook/core/views/authentication.py
index 2951b2b7f..86739871d 100644
--- a/passbook/core/views/authentication.py
+++ b/passbook/core/views/authentication.py
@@ -15,12 +15,12 @@ from structlog import get_logger
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up
-from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.flows.models import Flow, FlowDesignation
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.config import CONFIG
from passbook.lib.utils.urls import redirect_with_qs
+from passbook.stages.password.exceptions import PasswordPolicyInvalid
LOGGER = get_logger()
diff --git a/passbook/core/views/user.py b/passbook/core/views/user.py
index 6c4bdcfc6..0127266a9 100644
--- a/passbook/core/views/user.py
+++ b/passbook/core/views/user.py
@@ -10,8 +10,8 @@ from django.utils.translation import gettext as _
from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
-from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.lib.config import CONFIG
+from passbook.stages.password.exceptions import PasswordPolicyInvalid
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
diff --git a/passbook/factors/captcha/api.py b/passbook/factors/captcha/api.py
deleted file mode 100644
index 0786321b4..000000000
--- a/passbook/factors/captcha/api.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""CaptchaFactor API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.factors.captcha.models import CaptchaFactor
-
-
-class CaptchaFactorSerializer(ModelSerializer):
- """CaptchaFactor Serializer"""
-
- class Meta:
-
- model = CaptchaFactor
- fields = ["pk", "name", "slug", "order", "enabled", "public_key", "private_key"]
-
-
-class CaptchaFactorViewSet(ModelViewSet):
- """CaptchaFactor Viewset"""
-
- queryset = CaptchaFactor.objects.all()
- serializer_class = CaptchaFactorSerializer
diff --git a/passbook/factors/captcha/apps.py b/passbook/factors/captcha/apps.py
deleted file mode 100644
index d054894ae..000000000
--- a/passbook/factors/captcha/apps.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""passbook captcha app"""
-from django.apps import AppConfig
-
-
-class PassbookFactorCaptchaConfig(AppConfig):
- """passbook captcha app"""
-
- name = "passbook.factors.captcha"
- label = "passbook_factors_captcha"
- verbose_name = "passbook Factors.Captcha"
diff --git a/passbook/factors/captcha/forms.py b/passbook/factors/captcha/forms.py
deleted file mode 100644
index c19a3eeb4..000000000
--- a/passbook/factors/captcha/forms.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""passbook captcha factor forms"""
-from captcha.fields import ReCaptchaField
-from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
-from django.utils.translation import gettext_lazy as _
-
-from passbook.factors.captcha.models import CaptchaFactor
-from passbook.flows.forms import GENERAL_FIELDS
-
-
-class CaptchaForm(forms.Form):
- """passbook captcha factor form"""
-
- captcha = ReCaptchaField()
-
-
-class CaptchaFactorForm(forms.ModelForm):
- """Form to edit CaptchaFactor Instance"""
-
- class Meta:
-
- model = CaptchaFactor
- fields = GENERAL_FIELDS + ["public_key", "private_key"]
- widgets = {
- "name": forms.TextInput(),
- "order": forms.NumberInput(),
- "policies": FilteredSelectMultiple(_("policies"), False),
- "public_key": forms.TextInput(),
- "private_key": forms.TextInput(),
- }
- help_texts = {
- "policies": _(
- "Policies which determine if this factor applies to the current user."
- )
- }
diff --git a/passbook/factors/captcha/migrations/0001_initial.py b/passbook/factors/captcha/migrations/0001_initial.py
deleted file mode 100644
index 0f44952d8..000000000
--- a/passbook/factors/captcha/migrations/0001_initial.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Generated by Django 2.2.6 on 2019-10-07 14:07
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ("passbook_core", "0001_initial"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="CaptchaFactor",
- fields=[
- (
- "factor_ptr",
- models.OneToOneField(
- auto_created=True,
- on_delete=django.db.models.deletion.CASCADE,
- parent_link=True,
- primary_key=True,
- serialize=False,
- to="passbook_core.Factor",
- ),
- ),
- ("public_key", models.TextField()),
- ("private_key", models.TextField()),
- ],
- options={
- "verbose_name": "Captcha Factor",
- "verbose_name_plural": "Captcha Factors",
- },
- bases=("passbook_core.factor",),
- ),
- ]
diff --git a/passbook/factors/captcha/migrations/0002_auto_20200221_1410.py b/passbook/factors/captcha/migrations/0002_auto_20200221_1410.py
deleted file mode 100644
index 0960ee54d..000000000
--- a/passbook/factors/captcha/migrations/0002_auto_20200221_1410.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by Django 3.0.3 on 2020-02-21 14:10
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_factors_captcha", "0001_initial"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="captchafactor",
- name="private_key",
- field=models.TextField(
- help_text="Private key, acquired from https://www.google.com/recaptcha/intro/v3.html"
- ),
- ),
- migrations.AlterField(
- model_name="captchafactor",
- name="public_key",
- field=models.TextField(
- help_text="Public key, acquired from https://www.google.com/recaptcha/intro/v3.html"
- ),
- ),
- ]
diff --git a/passbook/factors/dummy/api.py b/passbook/factors/dummy/api.py
deleted file mode 100644
index 108698aaa..000000000
--- a/passbook/factors/dummy/api.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""DummyFactor API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.factors.dummy.models import DummyFactor
-
-
-class DummyFactorSerializer(ModelSerializer):
- """DummyFactor Serializer"""
-
- class Meta:
-
- model = DummyFactor
- fields = ["pk", "name", "slug", "order", "enabled"]
-
-
-class DummyFactorViewSet(ModelViewSet):
- """DummyFactor Viewset"""
-
- queryset = DummyFactor.objects.all()
- serializer_class = DummyFactorSerializer
diff --git a/passbook/factors/dummy/apps.py b/passbook/factors/dummy/apps.py
deleted file mode 100644
index 4cb858b88..000000000
--- a/passbook/factors/dummy/apps.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""passbook dummy factor config"""
-
-from django.apps import AppConfig
-
-
-class PassbookFactorDummyConfig(AppConfig):
- """passbook dummy factor config"""
-
- name = "passbook.factors.dummy"
- label = "passbook_factors_dummy"
- verbose_name = "passbook Factors.Dummy"
diff --git a/passbook/factors/dummy/factor.py b/passbook/factors/dummy/factor.py
deleted file mode 100644
index 5152c2753..000000000
--- a/passbook/factors/dummy/factor.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""passbook multi-factor authentication engine"""
-from django.http import HttpRequest
-
-from passbook.flows.factor_base import AuthenticationFactor
-
-
-class DummyFactor(AuthenticationFactor):
- """Dummy factor for testing with multiple factors"""
-
- def post(self, request: HttpRequest):
- """Just redirect to next factor"""
- return self.executor.factor_ok()
diff --git a/passbook/factors/dummy/forms.py b/passbook/factors/dummy/forms.py
deleted file mode 100644
index 72de002c9..000000000
--- a/passbook/factors/dummy/forms.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""passbook administration forms"""
-from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
-from django.utils.translation import gettext as _
-
-from passbook.factors.dummy.models import DummyFactor
-from passbook.flows.forms import GENERAL_FIELDS
-
-
-class DummyFactorForm(forms.ModelForm):
- """Form to create/edit Dummy Factor"""
-
- class Meta:
-
- model = DummyFactor
- fields = GENERAL_FIELDS
- widgets = {
- "name": forms.TextInput(),
- "order": forms.NumberInput(),
- "policies": FilteredSelectMultiple(_("policies"), False),
- }
diff --git a/passbook/factors/dummy/models.py b/passbook/factors/dummy/models.py
deleted file mode 100644
index a1e24d6ab..000000000
--- a/passbook/factors/dummy/models.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""dummy factor models"""
-from django.utils.translation import gettext as _
-
-from passbook.core.models import Factor
-
-
-class DummyFactor(Factor):
- """Dummy factor, mostly used to debug"""
-
- type = "passbook.factors.dummy.factor.DummyFactor"
- form = "passbook.factors.dummy.forms.DummyFactorForm"
-
- def __str__(self):
- return f"Dummy Factor {self.slug}"
-
- class Meta:
-
- verbose_name = _("Dummy Factor")
- verbose_name_plural = _("Dummy Factors")
diff --git a/passbook/factors/email/apps.py b/passbook/factors/email/apps.py
deleted file mode 100644
index 1382cf173..000000000
--- a/passbook/factors/email/apps.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""passbook email factor config"""
-from importlib import import_module
-
-from django.apps import AppConfig
-
-
-class PassbookFactorEmailConfig(AppConfig):
- """passbook email factor config"""
-
- name = "passbook.factors.email"
- label = "passbook_factors_email"
- verbose_name = "passbook Factors.Email"
-
- def ready(self):
- import_module("passbook.factors.email.tasks")
diff --git a/passbook/factors/email/migrations/0002_auto_20191011_1224.py b/passbook/factors/email/migrations/0002_auto_20191011_1224.py
deleted file mode 100644
index b020e7c5a..000000000
--- a/passbook/factors/email/migrations/0002_auto_20191011_1224.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2.6 on 2019-10-11 12:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_factors_email", "0001_initial"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="emailfactor",
- name="timeout",
- field=models.IntegerField(default=10),
- ),
- ]
diff --git a/passbook/factors/otp/api.py b/passbook/factors/otp/api.py
deleted file mode 100644
index 4962ddc46..000000000
--- a/passbook/factors/otp/api.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""OTPFactor API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.factors.otp.models import OTPFactor
-
-
-class OTPFactorSerializer(ModelSerializer):
- """OTPFactor Serializer"""
-
- class Meta:
-
- model = OTPFactor
- fields = ["pk", "name", "slug", "order", "enabled", "enforced"]
-
-
-class OTPFactorViewSet(ModelViewSet):
- """OTPFactor Viewset"""
-
- queryset = OTPFactor.objects.all()
- serializer_class = OTPFactorSerializer
diff --git a/passbook/factors/otp/apps.py b/passbook/factors/otp/apps.py
deleted file mode 100644
index d04263248..000000000
--- a/passbook/factors/otp/apps.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""passbook OTP AppConfig"""
-
-from django.apps.config import AppConfig
-
-
-class PassbookFactorOTPConfig(AppConfig):
- """passbook OTP AppConfig"""
-
- name = "passbook.factors.otp"
- label = "passbook_factors_otp"
- verbose_name = "passbook Factors.OTP"
- mountpoint = "user/otp/"
diff --git a/passbook/factors/otp/models.py b/passbook/factors/otp/models.py
deleted file mode 100644
index 1c87d6d69..000000000
--- a/passbook/factors/otp/models.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""OTP Factor"""
-from django.db import models
-from django.utils.translation import gettext as _
-
-from passbook.core.models import Factor
-from passbook.core.types import UIUserSettings
-
-
-class OTPFactor(Factor):
- """OTP Factor"""
-
- enforced = models.BooleanField(
- default=False,
- help_text=("Enforce enabled OTP for Users " "this factor applies to."),
- )
-
- type = "passbook.factors.otp.factors.OTPFactor"
- form = "passbook.factors.otp.forms.OTPFactorForm"
-
- @property
- def ui_user_settings(self) -> UIUserSettings:
- return UIUserSettings(
- name="OTP",
- icon="pficon-locked",
- view_name="passbook_factors_otp:otp-user-settings",
- )
-
- def __str__(self):
- return f"OTP Factor {self.slug}"
-
- class Meta:
-
- verbose_name = _("OTP Factor")
- verbose_name_plural = _("OTP Factors")
diff --git a/passbook/factors/password/api.py b/passbook/factors/password/api.py
deleted file mode 100644
index 1c8b3e407..000000000
--- a/passbook/factors/password/api.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""PasswordFactor API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.factors.password.models import PasswordFactor
-
-
-class PasswordFactorSerializer(ModelSerializer):
- """PasswordFactor Serializer"""
-
- class Meta:
-
- model = PasswordFactor
- fields = [
- "pk",
- "name",
- "slug",
- "order",
- "enabled",
- "backends",
- "password_policies",
- "reset_factors",
- ]
-
-
-class PasswordFactorViewSet(ModelViewSet):
- """PasswordFactor Viewset"""
-
- queryset = PasswordFactor.objects.all()
- serializer_class = PasswordFactorSerializer
diff --git a/passbook/factors/password/apps.py b/passbook/factors/password/apps.py
deleted file mode 100644
index 207d91f9a..000000000
--- a/passbook/factors/password/apps.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""passbook core app config"""
-from importlib import import_module
-
-from django.apps import AppConfig
-
-
-class PassbookFactorPasswordConfig(AppConfig):
- """passbook password factor config"""
-
- name = "passbook.factors.password"
- label = "passbook_factors_password"
- verbose_name = "passbook Factors.Password"
-
- def ready(self):
- import_module("passbook.factors.password.signals")
diff --git a/passbook/factors/password/migrations/0002_auto_20191007_1411.py b/passbook/factors/password/migrations/0002_auto_20191007_1411.py
deleted file mode 100644
index 3cafedf26..000000000
--- a/passbook/factors/password/migrations/0002_auto_20191007_1411.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2.6 on 2019-10-07 14:11
-
-from django.db import migrations
-
-
-def create_initial_factor(apps, schema_editor):
- """Create initial PasswordFactor if none exists"""
- PasswordFactor = apps.get_model("passbook_factors_password", "PasswordFactor")
- if not PasswordFactor.objects.exists():
- PasswordFactor.objects.create(
- name="password",
- slug="password",
- order=0,
- backends=["django.contrib.auth.backends.ModelBackend"],
- )
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_factors_password", "0001_initial"),
- ]
-
- operations = [migrations.RunPython(create_initial_factor)]
diff --git a/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py b/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py
deleted file mode 100644
index b64e40df8..000000000
--- a/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 2.2.6 on 2019-10-08 09:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_core", "0001_initial"),
- ("passbook_factors_password", "0002_auto_20191007_1411"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="passwordfactor",
- name="reset_factors",
- field=models.ManyToManyField(
- blank=True, related_name="reset_factors", to="passbook_core.Factor"
- ),
- ),
- ]
diff --git a/passbook/factors/password/migrations/0004_auto_20200221_1410.py b/passbook/factors/password/migrations/0004_auto_20200221_1410.py
deleted file mode 100644
index a34fdb1b9..000000000
--- a/passbook/factors/password/migrations/0004_auto_20200221_1410.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 3.0.3 on 2020-02-21 14:10
-
-import django.contrib.postgres.fields
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_factors_password", "0003_passwordfactor_reset_factors"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="passwordfactor",
- name="backends",
- field=django.contrib.postgres.fields.ArrayField(
- base_field=models.TextField(),
- help_text="Selection of backends to test the password against.",
- size=None,
- ),
- ),
- ]
diff --git a/passbook/factors/password/signals.py b/passbook/factors/password/signals.py
deleted file mode 100644
index ffdacda86..000000000
--- a/passbook/factors/password/signals.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""passbook password factor signals"""
-from django.dispatch import receiver
-
-from passbook.core.signals import password_changed
-from passbook.factors.password.exceptions import PasswordPolicyInvalid
-
-
-@receiver(password_changed)
-def password_policy_checker(sender, password, **_):
- """Run password through all password policies which are applied to the user"""
- from passbook.factors.password.models import PasswordFactor
- from passbook.policies.engine import PolicyEngine
-
- setattr(sender, "__password__", password)
- _all_factors = PasswordFactor.objects.filter(enabled=True).order_by("order")
- for factor in _all_factors:
- policy_engine = PolicyEngine(
- factor.password_policies.all().select_subclasses(), sender
- )
- policy_engine.build()
- passing, messages = policy_engine.result
- if not passing:
- raise PasswordPolicyInvalid(*messages)
diff --git a/passbook/flows/api.py b/passbook/flows/api.py
index 9edc3d9e5..bfb4bbde2 100644
--- a/passbook/flows/api.py
+++ b/passbook/flows/api.py
@@ -1,8 +1,8 @@
"""Flow API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
+from rest_framework.serializers import ModelSerializer, SerializerMethodField
+from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
-from passbook.flows.models import Flow, FlowFactorBinding
+from passbook.flows.models import Flow, FlowStageBinding, Stage
class FlowSerializer(ModelSerializer):
@@ -11,7 +11,7 @@ class FlowSerializer(ModelSerializer):
class Meta:
model = Flow
- fields = ["pk", "name", "slug", "designation", "factors", "policies"]
+ fields = ["pk", "name", "slug", "designation", "stages", "policies"]
class FlowViewSet(ModelViewSet):
@@ -21,17 +21,42 @@ class FlowViewSet(ModelViewSet):
serializer_class = FlowSerializer
-class FlowFactorBindingSerializer(ModelSerializer):
- """FlowFactorBinding Serializer"""
+class FlowStageBindingSerializer(ModelSerializer):
+ """FlowStageBinding Serializer"""
class Meta:
- model = FlowFactorBinding
- fields = ["pk", "flow", "factor", "re_evaluate_policies", "order", "policies"]
+ model = FlowStageBinding
+ fields = ["pk", "flow", "stage", "re_evaluate_policies", "order", "policies"]
-class FlowFactorBindingViewSet(ModelViewSet):
- """FlowFactorBinding Viewset"""
+class FlowStageBindingViewSet(ModelViewSet):
+ """FlowStageBinding Viewset"""
- queryset = FlowFactorBinding.objects.all()
- serializer_class = FlowFactorBindingSerializer
+ queryset = FlowStageBinding.objects.all()
+ serializer_class = FlowStageBindingSerializer
+
+
+class StageSerializer(ModelSerializer):
+ """Stage Serializer"""
+
+ __type__ = SerializerMethodField(method_name="get_type")
+
+ def get_type(self, obj):
+ """Get object type so that we know which API Endpoint to use to get the full object"""
+ return obj._meta.object_name.lower().replace("stage", "")
+
+ class Meta:
+
+ model = Stage
+ fields = ["pk", "name", "__type__"]
+
+
+class StageViewSet(ReadOnlyModelViewSet):
+ """Stage Viewset"""
+
+ queryset = Stage.objects.all()
+ serializer_class = StageSerializer
+
+ def get_queryset(self):
+ return Stage.objects.select_subclasses()
diff --git a/passbook/flows/forms.py b/passbook/flows/forms.py
index b7936891e..227679a10 100644
--- a/passbook/flows/forms.py
+++ b/passbook/flows/forms.py
@@ -1,12 +1,10 @@
-"""factor forms"""
+"""Flow and Stage forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
-from passbook.flows.models import Flow, FlowFactorBinding
-
-GENERAL_FIELDS = ["name", "slug", "order", "policies", "enabled"]
+from passbook.flows.models import Flow, FlowStageBinding
class FlowForm(forms.ModelForm):
@@ -19,29 +17,30 @@ class FlowForm(forms.ModelForm):
"name",
"slug",
"designation",
- "factors",
+ "stages",
"policies",
]
widgets = {
"name": forms.TextInput(),
- "factors": FilteredSelectMultiple(_("policies"), False),
+ "stages": FilteredSelectMultiple(_("stages"), False),
+ "policies": FilteredSelectMultiple(_("policies"), False),
}
-class FlowFactorBindingForm(forms.ModelForm):
- """FlowFactorBinding Form"""
+class FlowStageBindingForm(forms.ModelForm):
+ """FlowStageBinding Form"""
class Meta:
- model = FlowFactorBinding
+ model = FlowStageBinding
fields = [
"flow",
- "factor",
+ "stage",
"re_evaluate_policies",
"order",
"policies",
]
widgets = {
"name": forms.TextInput(),
- "factors": FilteredSelectMultiple(_("policies"), False),
+ "policies": FilteredSelectMultiple(_("policies"), False),
}
diff --git a/passbook/flows/migrations/0001_initial.py b/passbook/flows/migrations/0001_initial.py
index e07d2ce27..df7121778 100644
--- a/passbook/flows/migrations/0001_initial.py
+++ b/passbook/flows/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.0.3 on 2020-05-07 18:35
+# Generated by Django 3.0.3 on 2020-05-08 18:27
import uuid
@@ -11,8 +11,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ("passbook_policies", "0001_initial"),
- ("passbook_core", "0011_auto_20200222_1822"),
+ ("passbook_policies", "0003_auto_20200508_1642"),
]
operations = [
@@ -37,6 +36,7 @@ class Migration(migrations.Migration):
("AUTHENTICATION", "authentication"),
("ENROLLMENT", "enrollment"),
("RECOVERY", "recovery"),
+ ("PASSWORD_CHANGE", "password_change"),
],
max_length=100,
),
@@ -55,7 +55,23 @@ class Migration(migrations.Migration):
bases=("passbook_policies.policybindingmodel", models.Model),
),
migrations.CreateModel(
- name="FlowFactorBinding",
+ name="Stage",
+ fields=[
+ (
+ "uuid",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("name", models.TextField()),
+ ],
+ options={"abstract": False,},
+ ),
+ migrations.CreateModel(
+ name="FlowStageBinding",
fields=[
(
"policybindingmodel_ptr",
@@ -75,14 +91,14 @@ class Migration(migrations.Migration):
serialize=False,
),
),
- ("order", models.IntegerField()),
(
- "factor",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="passbook_core.Factor",
+ "re_evaluate_policies",
+ models.BooleanField(
+ default=False,
+ help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
),
),
+ ("order", models.IntegerField()),
(
"flow",
models.ForeignKey(
@@ -90,19 +106,29 @@ class Migration(migrations.Migration):
to="passbook_flows.Flow",
),
),
+ (
+ "stage",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="passbook_flows.Stage",
+ ),
+ ),
],
options={
- "verbose_name": "Flow Factor Binding",
- "verbose_name_plural": "Flow Factor Bindings",
- "unique_together": {("flow", "factor", "order")},
+ "verbose_name": "Flow Stage Binding",
+ "verbose_name_plural": "Flow Stage Bindings",
+ "ordering": ["order", "flow"],
+ "unique_together": {("flow", "stage", "order")},
},
bases=("passbook_policies.policybindingmodel", models.Model),
),
migrations.AddField(
model_name="flow",
- name="factors",
+ name="stages",
field=models.ManyToManyField(
- through="passbook_flows.FlowFactorBinding", to="passbook_core.Factor"
+ blank=True,
+ through="passbook_flows.FlowStageBinding",
+ to="passbook_flows.Stage",
),
),
]
diff --git a/passbook/flows/migrations/0004_default_flows.py b/passbook/flows/migrations/0002_default_flows.py
similarity index 58%
rename from passbook/flows/migrations/0004_default_flows.py
rename to passbook/flows/migrations/0002_default_flows.py
index c04d55df1..d2c326b7b 100644
--- a/passbook/flows/migrations/0004_default_flows.py
+++ b/passbook/flows/migrations/0002_default_flows.py
@@ -9,29 +9,35 @@ from passbook.flows.models import FlowDesignation
def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("passbook_flows", "Flow")
- FlowFactorBinding = apps.get_model("passbook_flows", "FlowFactorBinding")
- PasswordFactor = apps.get_model("passbook_factors_password", "PasswordFactor")
+ FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
+ PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
db_alias = schema_editor.connection.alias
if Flow.objects.using(db_alias).all().exists():
# Only create default flow when none exist
return
- pw_factor = PasswordFactor.objects.using(db_alias).first()
+ if not PasswordStage.objects.using(db_alias).exists():
+ PasswordStage.objects.using(db_alias).create(
+ name="password", backends=["django.contrib.auth.backends.ModelBackend"],
+ )
+
+ pw_stage = PasswordStage.objects.using(db_alias).first()
flow = Flow.objects.using(db_alias).create(
name="default-authentication-flow",
slug="default-authentication-flow",
designation=FlowDesignation.AUTHENTICATION,
)
- FlowFactorBinding.objects.using(db_alias).create(
- flow=flow, factor=pw_factor, order=0,
+ FlowStageBinding.objects.using(db_alias).create(
+ flow=flow, stage=pw_stage, order=0,
)
class Migration(migrations.Migration):
dependencies = [
- ("passbook_flows", "0003_auto_20200508_1230"),
+ ("passbook_flows", "0001_initial"),
+ ("passbook_stages_password", "0001_initial"),
]
operations = [migrations.RunPython(create_default_flow)]
diff --git a/passbook/flows/migrations/0002_flowfactorbinding_re_evaluate_policies.py b/passbook/flows/migrations/0002_flowfactorbinding_re_evaluate_policies.py
deleted file mode 100644
index bbd5bf15c..000000000
--- a/passbook/flows/migrations/0002_flowfactorbinding_re_evaluate_policies.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.0.3 on 2020-05-07 19:18
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_flows", "0001_initial"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="flowfactorbinding",
- name="re_evaluate_policies",
- field=models.BooleanField(
- default=False,
- help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
- ),
- ),
- ]
diff --git a/passbook/flows/migrations/0003_auto_20200508_1230.py b/passbook/flows/migrations/0003_auto_20200508_1230.py
deleted file mode 100644
index 645c7a53d..000000000
--- a/passbook/flows/migrations/0003_auto_20200508_1230.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.0.3 on 2020-05-08 12:30
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_flows", "0002_flowfactorbinding_re_evaluate_policies"),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name="flowfactorbinding",
- options={
- "ordering": ["order", "flow"],
- "verbose_name": "Flow Factor Binding",
- "verbose_name_plural": "Flow Factor Bindings",
- },
- ),
- ]
diff --git a/passbook/flows/migrations/0005_auto_20200508_1642.py b/passbook/flows/migrations/0005_auto_20200508_1642.py
deleted file mode 100644
index 3bf11a86e..000000000
--- a/passbook/flows/migrations/0005_auto_20200508_1642.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 3.0.3 on 2020-05-08 16:42
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_core", "0011_auto_20200222_1822"),
- ("passbook_flows", "0004_default_flows"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="flow",
- name="factors",
- field=models.ManyToManyField(
- blank=True,
- through="passbook_flows.FlowFactorBinding",
- to="passbook_core.Factor",
- ),
- ),
- ]
diff --git a/passbook/flows/models.py b/passbook/flows/models.py
index 5169fc1d8..164c14878 100644
--- a/passbook/flows/models.py
+++ b/passbook/flows/models.py
@@ -1,11 +1,12 @@
"""Flow models"""
from enum import Enum
-from typing import Tuple
+from typing import Optional, Tuple
from django.db import models
from django.utils.translation import gettext_lazy as _
+from model_utils.managers import InheritanceManager
-from passbook.core.models import Factor
+from passbook.core.types import UIUserSettings
from passbook.lib.models import UUIDModel
from passbook.policies.models import PolicyBindingModel
@@ -17,6 +18,7 @@ class FlowDesignation(Enum):
AUTHENTICATION = "authentication"
ENROLLMENT = "enrollment"
RECOVERY = "recovery"
+ PASSWORD_CHANGE = "password_change" # nosec # noqa
@staticmethod
def as_choices() -> Tuple[Tuple[str, str]]:
@@ -26,8 +28,28 @@ class FlowDesignation(Enum):
)
+class Stage(UUIDModel):
+ """Stage is an instance of a component used in a flow. This can verify the user,
+ enroll the user or offer a way of recovery"""
+
+ name = models.TextField()
+
+ objects = InheritanceManager()
+ type = ""
+ form = ""
+
+ @property
+ def ui_user_settings(self) -> Optional[UIUserSettings]:
+ """Entrypoint to integrate with User settings. Can either return None if no
+ user settings are available, or an instanace of UIUserSettings."""
+ return None
+
+ def __str__(self):
+ return f"Stage {self.name}"
+
+
class Flow(PolicyBindingModel, UUIDModel):
- """Flow describes how a series of Factors should be executed to authenticate/enroll/recover
+ """Flow describes how a series of Stages should be executed to authenticate/enroll/recover
a user. Additionally, policies can be applied, to specify which users
have access to this flow."""
@@ -36,7 +58,7 @@ class Flow(PolicyBindingModel, UUIDModel):
designation = models.CharField(max_length=100, choices=FlowDesignation.as_choices())
- factors = models.ManyToManyField(Factor, through="FlowFactorBinding", blank=True)
+ stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
pbm = models.OneToOneField(
PolicyBindingModel, parent_link=True, on_delete=models.CASCADE, related_name="+"
@@ -51,13 +73,13 @@ class Flow(PolicyBindingModel, UUIDModel):
verbose_name_plural = _("Flows")
-class FlowFactorBinding(PolicyBindingModel, UUIDModel):
- """Relationship between Flow and Factor. Order is required and unique for
- each flow-factor Binding. Additionally, policies can be specified, which determine if
+class FlowStageBinding(PolicyBindingModel, UUIDModel):
+ """Relationship between Flow and Stage. Order is required and unique for
+ each flow-stage Binding. Additionally, policies can be specified, which determine if
this Binding applies to the current user"""
flow = models.ForeignKey("Flow", on_delete=models.CASCADE)
- factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
+ stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
re_evaluate_policies = models.BooleanField(
default=False,
@@ -69,12 +91,12 @@ class FlowFactorBinding(PolicyBindingModel, UUIDModel):
order = models.IntegerField()
def __str__(self) -> str:
- return f"Flow Factor Binding #{self.order} {self.flow} -> {self.factor}"
+ return f"Flow Stage Binding #{self.order} {self.flow} -> {self.stage}"
class Meta:
ordering = ["order", "flow"]
- verbose_name = _("Flow Factor Binding")
- verbose_name_plural = _("Flow Factor Bindings")
- unique_together = (("flow", "factor", "order"),)
+ verbose_name = _("Flow Stage Binding")
+ verbose_name_plural = _("Flow Stage Bindings")
+ unique_together = (("flow", "stage", "order"),)
diff --git a/passbook/flows/planner.py b/passbook/flows/planner.py
index 7f0efa01a..b00f4f944 100644
--- a/passbook/flows/planner.py
+++ b/passbook/flows/planner.py
@@ -7,7 +7,7 @@ from django.http import HttpRequest
from structlog import get_logger
from passbook.flows.exceptions import FlowNonApplicableError
-from passbook.flows.models import Factor, Flow
+from passbook.flows.models import Flow, Stage
from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()
@@ -19,19 +19,19 @@ PLAN_CONTEXT_SSO = "is_sso"
@dataclass
class FlowPlan:
"""This data-class is the output of a FlowPlanner. It holds a flat list
- of all Factors that should be run."""
+ of all Stages that should be run."""
- factors: List[Factor] = field(default_factory=list)
+ stages: List[Stage] = field(default_factory=list)
context: Dict[str, Any] = field(default_factory=dict)
- def next(self) -> Factor:
- """Return next pending factor from the bottom of the list"""
- factor_cls = self.factors.pop(0)
- return factor_cls
+ def next(self) -> Stage:
+ """Return next pending stage from the bottom of the list"""
+ stage_cls = self.stages.pop(0)
+ return stage_cls
class FlowPlanner:
- """Execute all policies to plan out a flat list of all Factors
+ """Execute all policies to plan out a flat list of all Stages
that should be applied."""
flow: Flow
@@ -45,7 +45,7 @@ class FlowPlanner:
return engine.result
def plan(self, request: HttpRequest) -> FlowPlan:
- """Check each of the flows' policies, check policies for each factor with PolicyBinding
+ """Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
LOGGER.debug("Starting planning process", flow=self.flow)
start_time = time()
@@ -56,13 +56,18 @@ class FlowPlanner:
if not root_passing:
raise FlowNonApplicableError(root_passing_messages)
# Check Flow policies
- for factor in self.flow.factors.order_by("order").select_subclasses():
- engine = PolicyEngine(factor.policies.all(), request.user, request)
+ for stage in (
+ self.flow.stages.order_by("flowstagebinding__order")
+ .select_subclasses()
+ .select_related()
+ ):
+ binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk)
+ engine = PolicyEngine(binding.policies.all(), request.user, request)
engine.build()
passing, _ = engine.result
if passing:
- LOGGER.debug("Factor passing", factor=factor)
- plan.factors.append(factor)
+ LOGGER.debug("Stage passing", stage=stage)
+ plan.stages.append(stage)
end_time = time()
LOGGER.debug(
"Finished planning", flow=self.flow, duration_s=end_time - start_time
diff --git a/passbook/flows/factor_base.py b/passbook/flows/stage.py
similarity index 84%
rename from passbook/flows/factor_base.py
rename to passbook/flows/stage.py
index 001b444a2..804f9b325 100644
--- a/passbook/flows/factor_base.py
+++ b/passbook/flows/stage.py
@@ -1,4 +1,4 @@
-"""passbook multi-factor authentication engine"""
+"""passbook stage Base view"""
from typing import Any, Dict
from django.forms import ModelForm
@@ -11,8 +11,8 @@ from passbook.flows.views import FlowExecutorView
from passbook.lib.config import CONFIG
-class AuthenticationFactor(TemplateView):
- """Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
+class AuthenticationStage(TemplateView):
+ """Abstract Authentication stage, inherits TemplateView but can be combined with FormView"""
form: ModelForm = None
diff --git a/passbook/flows/views.py b/passbook/flows/views.py
index 1cf812d05..69de2c645 100644
--- a/passbook/flows/views.py
+++ b/passbook/flows/views.py
@@ -1,4 +1,4 @@
-"""passbook multi-factor authentication engine"""
+"""passbook multi-stage authentication engine"""
from typing import Optional
from django.contrib.auth import login
@@ -7,10 +7,9 @@ from django.shortcuts import get_object_or_404, redirect
from django.views.generic import View
from structlog import get_logger
-from passbook.core.models import Factor
from passbook.core.views.utils import PermissionDeniedView
from passbook.flows.exceptions import FlowNonApplicableError
-from passbook.flows.models import Flow
+from passbook.flows.models import Flow, Stage
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import class_to_path, path_to_class
@@ -24,13 +23,13 @@ SESSION_KEY_PLAN = "passbook_flows_plan"
class FlowExecutorView(View):
- """Stage 1 Flow executor, passing requests to Factor Views"""
+ """Stage 1 Flow executor, passing requests to Stage Views"""
flow: Flow
plan: FlowPlan
- current_factor: Factor
- current_factor_view: View
+ current_stage: Stage
+ current_stage_view: View
def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug)
@@ -77,36 +76,34 @@ class FlowExecutorView(View):
else:
LOGGER.debug("Continuing existing plan", flow_slug=flow_slug)
self.plan = self.request.session[SESSION_KEY_PLAN]
- # We don't save the Plan after getting the next factor
+ # We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
- self.current_factor = self.plan.next()
+ self.current_stage = self.plan.next()
LOGGER.debug(
- "Current factor",
- current_factor=self.current_factor,
- flow_slug=self.flow.slug,
+ "Current stage", current_stage=self.current_stage, flow_slug=self.flow.slug,
)
- factor_cls = path_to_class(self.current_factor.type)
- self.current_factor_view = factor_cls(self)
- self.current_factor_view.request = request
+ stage_cls = path_to_class(self.current_stage.type)
+ self.current_stage_view = stage_cls(self)
+ self.current_stage_view.request = request
return super().dispatch(request)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
- """pass get request to current factor"""
+ """pass get request to current stage"""
LOGGER.debug(
"Passing GET",
- view_class=class_to_path(self.current_factor_view.__class__),
+ view_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
)
- return self.current_factor_view.get(request, *args, **kwargs)
+ return self.current_stage_view.get(request, *args, **kwargs)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
- """pass post request to current factor"""
+ """pass post request to current stage"""
LOGGER.debug(
"Passing POST",
- view_class=class_to_path(self.current_factor_view.__class__),
+ view_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
)
- return self.current_factor_view.post(request, *args, **kwargs)
+ return self.current_stage_view.post(request, *args, **kwargs)
def _initiate_plan(self) -> FlowPlan:
planner = FlowPlanner(self.flow)
@@ -115,7 +112,7 @@ class FlowExecutorView(View):
return plan
def _flow_done(self) -> HttpResponse:
- """User Successfully passed all factors"""
+ """User Successfully passed all stages"""
backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend
login(
self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend
@@ -131,34 +128,34 @@ class FlowExecutorView(View):
return redirect(next_param)
return redirect_with_qs("passbook_core:overview")
- def factor_ok(self) -> HttpResponse:
- """Callback called by factors upon successful completion.
+ def stage_ok(self) -> HttpResponse:
+ """Callback called by stages upon successful completion.
Persists updated plan and context to session."""
LOGGER.debug(
- "Factor ok",
- factor_class=class_to_path(self.current_factor_view.__class__),
+ "Stage ok",
+ stage_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
)
self.request.session[SESSION_KEY_PLAN] = self.plan
- if self.plan.factors:
+ if self.plan.stages:
LOGGER.debug(
- "Continuing with next factor",
- reamining=len(self.plan.factors),
+ "Continuing with next stage",
+ reamining=len(self.plan.stages),
flow_slug=self.flow.slug,
)
return redirect_with_qs(
"passbook_flows:flow-executor", self.request.GET, **self.kwargs
)
- # User passed all factors
+ # User passed all stages
LOGGER.debug(
- "User passed all factors",
+ "User passed all stages",
user=self.plan.context[PLAN_CONTEXT_PENDING_USER],
flow_slug=self.flow.slug,
)
return self._flow_done()
- def factor_invalid(self) -> HttpResponse:
- """Callback used factor when data is correct but a policy denies access
+ def stage_invalid(self) -> HttpResponse:
+ """Callback used stage when data is correct but a policy denies access
or the user account is disabled."""
LOGGER.debug("User invalid", flow_slug=self.flow.slug)
self.cancel()
diff --git a/passbook/root/settings.py b/passbook/root/settings.py
index 9b22a0174..9e8c910e9 100644
--- a/passbook/root/settings.py
+++ b/passbook/root/settings.py
@@ -96,11 +96,11 @@ INSTALLED_APPS = [
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
"passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
- "passbook.factors.otp.apps.PassbookFactorOTPConfig",
- "passbook.factors.captcha.apps.PassbookFactorCaptchaConfig",
- "passbook.factors.password.apps.PassbookFactorPasswordConfig",
- "passbook.factors.dummy.apps.PassbookFactorDummyConfig",
- "passbook.factors.email.apps.PassbookFactorEmailConfig",
+ "passbook.stages.otp.apps.PassbookStageOTPConfig",
+ "passbook.stages.captcha.apps.PassbookStageCaptchaConfig",
+ "passbook.stages.password.apps.PassbookStagePasswordConfig",
+ "passbook.stages.dummy.apps.PassbookStageDummyConfig",
+ "passbook.stages.email.apps.PassbookStageEmailConfig",
"passbook.policies.expiry.apps.PassbookPolicyExpiryConfig",
"passbook.policies.reputation.apps.PassbookPolicyReputationConfig",
"passbook.policies.hibp.apps.PassbookPolicyHIBPConfig",
diff --git a/passbook/sources/oauth/views/core.py b/passbook/sources/oauth/views/core.py
index e794fff0e..36e84d869 100644
--- a/passbook/sources/oauth/views/core.py
+++ b/passbook/sources/oauth/views/core.py
@@ -13,7 +13,6 @@ from django.views.generic import RedirectView, View
from structlog import get_logger
from passbook.audit.models import Event, EventAction
-from passbook.factors.password.factor import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from passbook.flows.models import Flow, FlowDesignation
from passbook.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
@@ -24,6 +23,7 @@ from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs
from passbook.sources.oauth.clients import get_client
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
+from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
LOGGER = get_logger()
@@ -169,7 +169,7 @@ class OAuthCallback(OAuthClientMixin, View):
return None
def handle_login(self, user, source, access):
- """Prepare AuthenticationView, redirect users to remaining Factors"""
+ """Prepare Authentication Plan, redirect user FlowExecutor"""
user = authenticate(
source=access.source, identifier=access.identifier, request=self.request
)
diff --git a/passbook/factors/__init__.py b/passbook/stages/__init__.py
similarity index 100%
rename from passbook/factors/__init__.py
rename to passbook/stages/__init__.py
diff --git a/passbook/factors/captcha/__init__.py b/passbook/stages/captcha/__init__.py
similarity index 100%
rename from passbook/factors/captcha/__init__.py
rename to passbook/stages/captcha/__init__.py
diff --git a/passbook/stages/captcha/api.py b/passbook/stages/captcha/api.py
new file mode 100644
index 000000000..6357fc96a
--- /dev/null
+++ b/passbook/stages/captcha/api.py
@@ -0,0 +1,21 @@
+"""CaptchaStage API Views"""
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import ModelViewSet
+
+from passbook.stages.captcha.models import CaptchaStage
+
+
+class CaptchaStageSerializer(ModelSerializer):
+ """CaptchaStage Serializer"""
+
+ class Meta:
+
+ model = CaptchaStage
+ fields = ["pk", "name", "public_key", "private_key"]
+
+
+class CaptchaStageViewSet(ModelViewSet):
+ """CaptchaStage Viewset"""
+
+ queryset = CaptchaStage.objects.all()
+ serializer_class = CaptchaStageSerializer
diff --git a/passbook/stages/captcha/apps.py b/passbook/stages/captcha/apps.py
new file mode 100644
index 000000000..ba5c4ba7d
--- /dev/null
+++ b/passbook/stages/captcha/apps.py
@@ -0,0 +1,10 @@
+"""passbook captcha app"""
+from django.apps import AppConfig
+
+
+class PassbookStageCaptchaConfig(AppConfig):
+ """passbook captcha app"""
+
+ name = "passbook.stages.captcha"
+ label = "passbook_stages_captcha"
+ verbose_name = "passbook Stages.Captcha"
diff --git a/passbook/stages/captcha/forms.py b/passbook/stages/captcha/forms.py
new file mode 100644
index 000000000..892942a4a
--- /dev/null
+++ b/passbook/stages/captcha/forms.py
@@ -0,0 +1,25 @@
+"""passbook captcha stage forms"""
+from captcha.fields import ReCaptchaField
+from django import forms
+
+from passbook.stages.captcha.models import CaptchaStage
+
+
+class CaptchaForm(forms.Form):
+ """passbook captcha stage form"""
+
+ captcha = ReCaptchaField()
+
+
+class CaptchaStageForm(forms.ModelForm):
+ """Form to edit CaptchaStage Instance"""
+
+ class Meta:
+
+ model = CaptchaStage
+ fields = ["name", "public_key", "private_key"]
+ widgets = {
+ "name": forms.TextInput(),
+ "public_key": forms.TextInput(),
+ "private_key": forms.TextInput(),
+ }
diff --git a/passbook/stages/captcha/migrations/0001_initial.py b/passbook/stages/captcha/migrations/0001_initial.py
new file mode 100644
index 000000000..7a3fbb2ba
--- /dev/null
+++ b/passbook/stages/captcha/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# Generated by Django 3.0.3 on 2020-05-08 17:58
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("passbook_flows", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="CaptchaStage",
+ fields=[
+ (
+ "stage_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="passbook_flows.Stage",
+ ),
+ ),
+ (
+ "public_key",
+ models.TextField(
+ help_text="Public key, acquired from https://www.google.com/recaptcha/intro/v3.html"
+ ),
+ ),
+ (
+ "private_key",
+ models.TextField(
+ help_text="Private key, acquired from https://www.google.com/recaptcha/intro/v3.html"
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Captcha Stage",
+ "verbose_name_plural": "Captcha Stages",
+ },
+ bases=("passbook_flows.stage",),
+ ),
+ ]
diff --git a/passbook/factors/captcha/migrations/__init__.py b/passbook/stages/captcha/migrations/__init__.py
similarity index 100%
rename from passbook/factors/captcha/migrations/__init__.py
rename to passbook/stages/captcha/migrations/__init__.py
diff --git a/passbook/factors/captcha/models.py b/passbook/stages/captcha/models.py
similarity index 53%
rename from passbook/factors/captcha/models.py
rename to passbook/stages/captcha/models.py
index 3619bb6e5..492ac2f35 100644
--- a/passbook/factors/captcha/models.py
+++ b/passbook/stages/captcha/models.py
@@ -1,12 +1,12 @@
-"""passbook captcha factor"""
+"""passbook captcha stage"""
from django.db import models
from django.utils.translation import gettext_lazy as _
-from passbook.core.models import Factor
+from passbook.flows.models import Stage
-class CaptchaFactor(Factor):
- """Captcha Factor instance"""
+class CaptchaStage(Stage):
+ """Captcha Stage instance"""
public_key = models.TextField(
help_text=_(
@@ -19,13 +19,13 @@ class CaptchaFactor(Factor):
)
)
- type = "passbook.factors.captcha.factor.CaptchaFactor"
- form = "passbook.factors.captcha.forms.CaptchaFactorForm"
+ type = "passbook.stages.captcha.stage.CaptchaStage"
+ form = "passbook.stages.captcha.forms.CaptchaStageForm"
def __str__(self):
- return f"Captcha Factor {self.slug}"
+ return f"Captcha Stage {self.name}"
class Meta:
- verbose_name = _("Captcha Factor")
- verbose_name_plural = _("Captcha Factors")
+ verbose_name = _("Captcha Stage")
+ verbose_name_plural = _("Captcha Stages")
diff --git a/passbook/factors/captcha/settings.py b/passbook/stages/captcha/settings.py
similarity index 90%
rename from passbook/factors/captcha/settings.py
rename to passbook/stages/captcha/settings.py
index 7466ad4f8..17b1b3829 100644
--- a/passbook/factors/captcha/settings.py
+++ b/passbook/stages/captcha/settings.py
@@ -1,4 +1,4 @@
-"""passbook captcha_factor settings"""
+"""passbook captcha stage settings"""
# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
diff --git a/passbook/factors/captcha/factor.py b/passbook/stages/captcha/stage.py
similarity index 66%
rename from passbook/factors/captcha/factor.py
rename to passbook/stages/captcha/stage.py
index 130d6916b..2d95b8232 100644
--- a/passbook/factors/captcha/factor.py
+++ b/passbook/stages/captcha/stage.py
@@ -1,23 +1,23 @@
-"""passbook captcha factor"""
+"""passbook captcha stage"""
from django.views.generic import FormView
-from passbook.factors.captcha.forms import CaptchaForm
-from passbook.flows.factor_base import AuthenticationFactor
+from passbook.flows.stage import AuthenticationStage
+from passbook.stages.captcha.forms import CaptchaForm
-class CaptchaFactor(FormView, AuthenticationFactor):
+class CaptchaStage(FormView, AuthenticationStage):
"""Simple captcha checker, logic is handeled in django-captcha module"""
form_class = CaptchaForm
def form_valid(self, form):
- return self.executor.factor_ok()
+ return self.executor.stage_ok()
def get_form(self, form_class=None):
form = CaptchaForm(**self.get_form_kwargs())
- form.fields["captcha"].public_key = self.executor.current_factor.public_key
- form.fields["captcha"].private_key = self.executor.current_factor.private_key
+ form.fields["captcha"].public_key = self.executor.current_stage.public_key
+ form.fields["captcha"].private_key = self.executor.current_stage.private_key
form.fields["captcha"].widget.attrs["data-sitekey"] = form.fields[
"captcha"
].public_key
diff --git a/passbook/factors/dummy/__init__.py b/passbook/stages/dummy/__init__.py
similarity index 100%
rename from passbook/factors/dummy/__init__.py
rename to passbook/stages/dummy/__init__.py
diff --git a/passbook/stages/dummy/api.py b/passbook/stages/dummy/api.py
new file mode 100644
index 000000000..53235a3b9
--- /dev/null
+++ b/passbook/stages/dummy/api.py
@@ -0,0 +1,21 @@
+"""DummyStage API Views"""
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import ModelViewSet
+
+from passbook.stages.dummy.models import DummyStage
+
+
+class DummyStageSerializer(ModelSerializer):
+ """DummyStage Serializer"""
+
+ class Meta:
+
+ model = DummyStage
+ fields = ["pk", "name"]
+
+
+class DummyStageViewSet(ModelViewSet):
+ """DummyStage Viewset"""
+
+ queryset = DummyStage.objects.all()
+ serializer_class = DummyStageSerializer
diff --git a/passbook/stages/dummy/apps.py b/passbook/stages/dummy/apps.py
new file mode 100644
index 000000000..5b68b9459
--- /dev/null
+++ b/passbook/stages/dummy/apps.py
@@ -0,0 +1,11 @@
+"""passbook dummy stage config"""
+
+from django.apps import AppConfig
+
+
+class PassbookStageDummyConfig(AppConfig):
+ """passbook dummy stage config"""
+
+ name = "passbook.stages.dummy"
+ label = "passbook_stages_dummy"
+ verbose_name = "passbook Stages.Dummy"
diff --git a/passbook/stages/dummy/forms.py b/passbook/stages/dummy/forms.py
new file mode 100644
index 000000000..f611611f5
--- /dev/null
+++ b/passbook/stages/dummy/forms.py
@@ -0,0 +1,16 @@
+"""passbook administration forms"""
+from django import forms
+
+from passbook.stages.dummy.models import DummyStage
+
+
+class DummyStageForm(forms.ModelForm):
+ """Form to create/edit Dummy Stage"""
+
+ class Meta:
+
+ model = DummyStage
+ fields = ["name"]
+ widgets = {
+ "name": forms.TextInput(),
+ }
diff --git a/passbook/factors/dummy/migrations/0001_initial.py b/passbook/stages/dummy/migrations/0001_initial.py
similarity index 64%
rename from passbook/factors/dummy/migrations/0001_initial.py
rename to passbook/stages/dummy/migrations/0001_initial.py
index d0a905a87..83b2eea91 100644
--- a/passbook/factors/dummy/migrations/0001_initial.py
+++ b/passbook/stages/dummy/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.6 on 2019-10-07 14:07
+# Generated by Django 3.0.3 on 2020-05-08 17:58
import django.db.models.deletion
from django.db import migrations, models
@@ -9,29 +9,29 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ("passbook_core", "0001_initial"),
+ ("passbook_flows", "0001_initial"),
]
operations = [
migrations.CreateModel(
- name="DummyFactor",
+ name="DummyStage",
fields=[
(
- "factor_ptr",
+ "stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
- to="passbook_core.Factor",
+ to="passbook_flows.Stage",
),
),
],
options={
- "verbose_name": "Dummy Factor",
- "verbose_name_plural": "Dummy Factors",
+ "verbose_name": "Dummy Stage",
+ "verbose_name_plural": "Dummy Stages",
},
- bases=("passbook_core.factor",),
+ bases=("passbook_flows.stage",),
),
]
diff --git a/passbook/factors/dummy/migrations/__init__.py b/passbook/stages/dummy/migrations/__init__.py
similarity index 100%
rename from passbook/factors/dummy/migrations/__init__.py
rename to passbook/stages/dummy/migrations/__init__.py
diff --git a/passbook/stages/dummy/models.py b/passbook/stages/dummy/models.py
new file mode 100644
index 000000000..70457e217
--- /dev/null
+++ b/passbook/stages/dummy/models.py
@@ -0,0 +1,19 @@
+"""dummy stage models"""
+from django.utils.translation import gettext as _
+
+from passbook.flows.models import Stage
+
+
+class DummyStage(Stage):
+ """Dummy stage, mostly used to debug"""
+
+ type = "passbook.stages.dummy.stage.DummyStage"
+ form = "passbook.stages.dummy.forms.DummyStageForm"
+
+ def __str__(self):
+ return f"Dummy Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("Dummy Stage")
+ verbose_name_plural = _("Dummy Stages")
diff --git a/passbook/stages/dummy/stage.py b/passbook/stages/dummy/stage.py
new file mode 100644
index 000000000..94a2a4cb6
--- /dev/null
+++ b/passbook/stages/dummy/stage.py
@@ -0,0 +1,12 @@
+"""passbook multi-stage authentication engine"""
+from django.http import HttpRequest
+
+from passbook.flows.stage import AuthenticationStage
+
+
+class DummyStage(AuthenticationStage):
+ """Dummy stage for testing with multiple stages"""
+
+ def post(self, request: HttpRequest):
+ """Just redirect to next stage"""
+ return self.executor.stage_ok()
diff --git a/passbook/factors/email/__init__.py b/passbook/stages/email/__init__.py
similarity index 100%
rename from passbook/factors/email/__init__.py
rename to passbook/stages/email/__init__.py
diff --git a/passbook/factors/email/api.py b/passbook/stages/email/api.py
similarity index 54%
rename from passbook/factors/email/api.py
rename to passbook/stages/email/api.py
index 165ce9064..14e6c9a3c 100644
--- a/passbook/factors/email/api.py
+++ b/passbook/stages/email/api.py
@@ -1,22 +1,19 @@
-"""EmailFactor API Views"""
+"""EmailStage API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
-from passbook.factors.email.models import EmailFactor
+from passbook.stages.email.models import EmailStage
-class EmailFactorSerializer(ModelSerializer):
- """EmailFactor Serializer"""
+class EmailStageSerializer(ModelSerializer):
+ """EmailStage Serializer"""
class Meta:
- model = EmailFactor
+ model = EmailStage
fields = [
"pk",
"name",
- "slug",
- "order",
- "enabled",
"host",
"port",
"username",
@@ -31,8 +28,8 @@ class EmailFactorSerializer(ModelSerializer):
extra_kwargs = {"password": {"write_only": True}}
-class EmailFactorViewSet(ModelViewSet):
- """EmailFactor Viewset"""
+class EmailStageViewSet(ModelViewSet):
+ """EmailStage Viewset"""
- queryset = EmailFactor.objects.all()
- serializer_class = EmailFactorSerializer
+ queryset = EmailStage.objects.all()
+ serializer_class = EmailStageSerializer
diff --git a/passbook/stages/email/apps.py b/passbook/stages/email/apps.py
new file mode 100644
index 000000000..241904ee5
--- /dev/null
+++ b/passbook/stages/email/apps.py
@@ -0,0 +1,15 @@
+"""passbook email stage config"""
+from importlib import import_module
+
+from django.apps import AppConfig
+
+
+class PassbookStageEmailConfig(AppConfig):
+ """passbook email stage config"""
+
+ name = "passbook.stages.email"
+ label = "passbook_stages_email"
+ verbose_name = "passbook Stages.Email"
+
+ def ready(self):
+ import_module("passbook.stages.email.tasks")
diff --git a/passbook/factors/email/forms.py b/passbook/stages/email/forms.py
similarity index 60%
rename from passbook/factors/email/forms.py
rename to passbook/stages/email/forms.py
index 8429a0870..ae93d428e 100644
--- a/passbook/factors/email/forms.py
+++ b/passbook/stages/email/forms.py
@@ -1,19 +1,18 @@
"""passbook administration forms"""
from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
-from passbook.factors.email.models import EmailFactor
-from passbook.flows.forms import GENERAL_FIELDS
+from passbook.stages.email.models import EmailStage
-class EmailFactorForm(forms.ModelForm):
- """Form to create/edit Dummy Factor"""
+class EmailStageForm(forms.ModelForm):
+ """Form to create/edit Dummy Stage"""
class Meta:
- model = EmailFactor
- fields = GENERAL_FIELDS + [
+ model = EmailStage
+ fields = [
+ "name",
"host",
"port",
"username",
@@ -27,8 +26,6 @@ class EmailFactorForm(forms.ModelForm):
]
widgets = {
"name": forms.TextInput(),
- "order": forms.NumberInput(),
- "policies": FilteredSelectMultiple(_("policies"), False),
"host": forms.TextInput(),
"username": forms.TextInput(),
"password": forms.TextInput(),
@@ -41,8 +38,3 @@ class EmailFactorForm(forms.ModelForm):
"ssl_keyfile": _("SSL Keyfile (optional)"),
"ssl_certfile": _("SSL Certfile (optional)"),
}
- help_texts = {
- "policies": _(
- "Policies which determine if this factor applies to the current user."
- )
- }
diff --git a/passbook/factors/email/migrations/0001_initial.py b/passbook/stages/email/migrations/0001_initial.py
similarity index 76%
rename from passbook/factors/email/migrations/0001_initial.py
rename to passbook/stages/email/migrations/0001_initial.py
index e9b8519b2..940c614af 100644
--- a/passbook/factors/email/migrations/0001_initial.py
+++ b/passbook/stages/email/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.6 on 2019-10-08 12:23
+# Generated by Django 3.0.3 on 2020-05-08 17:59
import django.db.models.deletion
from django.db import migrations, models
@@ -9,22 +9,22 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ("passbook_core", "0001_initial"),
+ ("passbook_flows", "0001_initial"),
]
operations = [
migrations.CreateModel(
- name="EmailFactor",
+ name="EmailStage",
fields=[
(
- "factor_ptr",
+ "stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
- to="passbook_core.Factor",
+ to="passbook_flows.Stage",
),
),
("host", models.TextField(default="localhost")),
@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
("password", models.TextField(blank=True, default="")),
("use_tls", models.BooleanField(default=False)),
("use_ssl", models.BooleanField(default=False)),
- ("timeout", models.IntegerField(default=0)),
+ ("timeout", models.IntegerField(default=10)),
("ssl_keyfile", models.TextField(blank=True, default=None, null=True)),
("ssl_certfile", models.TextField(blank=True, default=None, null=True)),
(
@@ -42,9 +42,9 @@ class Migration(migrations.Migration):
),
],
options={
- "verbose_name": "Email Factor",
- "verbose_name_plural": "Email Factors",
+ "verbose_name": "Email Stage",
+ "verbose_name_plural": "Email Stages",
},
- bases=("passbook_core.factor",),
+ bases=("passbook_flows.stage",),
),
]
diff --git a/passbook/factors/email/migrations/__init__.py b/passbook/stages/email/migrations/__init__.py
similarity index 100%
rename from passbook/factors/email/migrations/__init__.py
rename to passbook/stages/email/migrations/__init__.py
diff --git a/passbook/factors/email/models.py b/passbook/stages/email/models.py
similarity index 76%
rename from passbook/factors/email/models.py
rename to passbook/stages/email/models.py
index f2027c1d9..57a104ed8 100644
--- a/passbook/factors/email/models.py
+++ b/passbook/stages/email/models.py
@@ -1,13 +1,13 @@
-"""email factor models"""
+"""email stage models"""
from django.core.mail.backends.smtp import EmailBackend
from django.db import models
from django.utils.translation import gettext as _
-from passbook.core.models import Factor
+from passbook.flows.models import Stage
-class EmailFactor(Factor):
- """email factor"""
+class EmailStage(Stage):
+ """email stage"""
host = models.TextField(default="localhost")
port = models.IntegerField(default=25)
@@ -22,8 +22,8 @@ class EmailFactor(Factor):
from_address = models.EmailField(default="system@passbook.local")
- type = "passbook.factors.email.factor.EmailFactorView"
- form = "passbook.factors.email.forms.EmailFactorForm"
+ type = "passbook.stages.email.stage.EmailStageView"
+ form = "passbook.stages.email.forms.EmailStageForm"
@property
def backend(self) -> EmailBackend:
@@ -41,9 +41,9 @@ class EmailFactor(Factor):
)
def __str__(self):
- return f"Email Factor {self.slug}"
+ return f"Email Stage {self.name}"
class Meta:
- verbose_name = _("Email Factor")
- verbose_name_plural = _("Email Factors")
+ verbose_name = _("Email Stage")
+ verbose_name_plural = _("Email Stages")
diff --git a/passbook/factors/email/factor.py b/passbook/stages/email/stage.py
similarity index 75%
rename from passbook/factors/email/factor.py
rename to passbook/stages/email/stage.py
index 8c7d0527a..129e57d4f 100644
--- a/passbook/factors/email/factor.py
+++ b/passbook/stages/email/stage.py
@@ -1,4 +1,4 @@
-"""passbook multi-factor authentication engine"""
+"""passbook multi-stage authentication engine"""
from django.contrib import messages
from django.http import HttpRequest
from django.shortcuts import reverse
@@ -6,17 +6,17 @@ from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Nonce
-from passbook.factors.email.tasks import send_mails
-from passbook.factors.email.utils import TemplateEmailMessage
-from passbook.flows.factor_base import AuthenticationFactor
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
+from passbook.flows.stage import AuthenticationStage
from passbook.lib.config import CONFIG
+from passbook.stages.email.tasks import send_mails
+from passbook.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
-class EmailFactorView(AuthenticationFactor):
- """Dummy factor for testing with multiple factors"""
+class EmailStageView(AuthenticationStage):
+ """E-Mail stage which sends E-Mail for verification"""
def get_context_data(self, **kwargs):
kwargs["show_password_forget_notice"] = CONFIG.y(
@@ -41,10 +41,10 @@ class EmailFactorView(AuthenticationFactor):
)
},
)
- send_mails(self.executor.current_factor, message)
+ send_mails(self.executor.current_stage, message)
messages.success(request, _("Check your E-Mails for a password reset link."))
return self.executor.cancel()
def post(self, request: HttpRequest):
- """Just redirect to next factor"""
- return self.executor.factor_ok()
+ """Just redirect to next stage"""
+ return self.executor.stage_ok()
diff --git a/passbook/factors/email/tasks.py b/passbook/stages/email/tasks.py
similarity index 62%
rename from passbook/factors/email/tasks.py
rename to passbook/stages/email/tasks.py
index b3e752b04..265f69cb2 100644
--- a/passbook/factors/email/tasks.py
+++ b/passbook/stages/email/tasks.py
@@ -1,4 +1,4 @@
-"""email factor tasks"""
+"""email stage tasks"""
from smtplib import SMTPException
from typing import Any, Dict, List
@@ -6,38 +6,38 @@ from celery import group
from django.core.mail import EmailMessage
from structlog import get_logger
-from passbook.factors.email.models import EmailFactor
from passbook.root.celery import CELERY_APP
+from passbook.stages.email.models import EmailStage
LOGGER = get_logger()
-def send_mails(factor: EmailFactor, *messages: List[EmailMessage]):
+def send_mails(stage: EmailStage, *messages: List[EmailMessage]):
"""Wrapper to convert EmailMessage to dict and send it from worker"""
tasks = []
for message in messages:
- tasks.append(_send_mail_task.s(factor.pk, message.__dict__))
+ tasks.append(_send_mail_task.s(stage.pk, message.__dict__))
lazy_group = group(*tasks)
promise = lazy_group()
return promise
@CELERY_APP.task(bind=True)
-def _send_mail_task(self, email_factor_pk: int, message: Dict[Any, Any]):
- """Send E-Mail according to EmailFactor parameters from background worker.
+def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
+ """Send E-Mail according to EmailStage parameters from background worker.
Automatically retries if message couldn't be sent."""
- factor: EmailFactor = EmailFactor.objects.get(pk=email_factor_pk)
- backend = factor.backend
+ stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
+ backend = stage.backend
backend.open()
# Since django's EmailMessage objects are not JSON serialisable,
# we need to rebuild them from a dict
message_object = EmailMessage()
for key, value in message.items():
setattr(message_object, key, value)
- message_object.from_email = factor.from_address
+ message_object.from_email = stage.from_address
LOGGER.debug("Sending mail", to=message_object.to)
try:
- num_sent = factor.backend.send_messages([message_object])
+ num_sent = stage.backend.send_messages([message_object])
except SMTPException as exc:
raise self.retry(exc=exc)
if num_sent != 1:
diff --git a/passbook/factors/email/utils.py b/passbook/stages/email/utils.py
similarity index 100%
rename from passbook/factors/email/utils.py
rename to passbook/stages/email/utils.py
diff --git a/passbook/factors/otp/__init__.py b/passbook/stages/otp/__init__.py
similarity index 100%
rename from passbook/factors/otp/__init__.py
rename to passbook/stages/otp/__init__.py
diff --git a/passbook/stages/otp/api.py b/passbook/stages/otp/api.py
new file mode 100644
index 000000000..9378a4cb0
--- /dev/null
+++ b/passbook/stages/otp/api.py
@@ -0,0 +1,21 @@
+"""OTPStage API Views"""
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import ModelViewSet
+
+from passbook.stages.otp.models import OTPStage
+
+
+class OTPStageSerializer(ModelSerializer):
+ """OTPStage Serializer"""
+
+ class Meta:
+
+ model = OTPStage
+ fields = ["pk", "name", "enforced"]
+
+
+class OTPStageViewSet(ModelViewSet):
+ """OTPStage Viewset"""
+
+ queryset = OTPStage.objects.all()
+ serializer_class = OTPStageSerializer
diff --git a/passbook/stages/otp/apps.py b/passbook/stages/otp/apps.py
new file mode 100644
index 000000000..88b5ce441
--- /dev/null
+++ b/passbook/stages/otp/apps.py
@@ -0,0 +1,12 @@
+"""passbook OTP AppConfig"""
+
+from django.apps.config import AppConfig
+
+
+class PassbookStageOTPConfig(AppConfig):
+ """passbook OTP AppConfig"""
+
+ name = "passbook.stages.otp"
+ label = "passbook_stages_otp"
+ verbose_name = "passbook Stages.OTP"
+ mountpoint = "user/otp/"
diff --git a/passbook/factors/otp/forms.py b/passbook/stages/otp/forms.py
similarity index 77%
rename from passbook/factors/otp/forms.py
rename to passbook/stages/otp/forms.py
index c6c6816a5..0033667cf 100644
--- a/passbook/factors/otp/forms.py
+++ b/passbook/stages/otp/forms.py
@@ -1,14 +1,12 @@
"""passbook OTP Forms"""
from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
-from passbook.factors.otp.models import OTPFactor
-from passbook.flows.forms import GENERAL_FIELDS
+from passbook.stages.otp.models import OTPStage
OTP_CODE_VALIDATOR = RegexValidator(
r"^[0-9a-z]{6,8}$", _("Only alpha-numeric characters are allowed.")
@@ -68,20 +66,13 @@ class OTPSetupForm(forms.Form):
return self.cleaned_data.get("code")
-class OTPFactorForm(forms.ModelForm):
- """Form to edit OTPFactor instances"""
+class OTPStageForm(forms.ModelForm):
+ """Form to edit OTPStage instances"""
class Meta:
- model = OTPFactor
- fields = GENERAL_FIELDS + ["enforced"]
+ model = OTPStage
+ fields = ["name", "enforced"]
widgets = {
"name": forms.TextInput(),
- "order": forms.NumberInput(),
- "policies": FilteredSelectMultiple(_("policies"), False),
- }
- help_texts = {
- "policies": _(
- "Policies which determine if this factor applies to the current user."
- )
}
diff --git a/passbook/factors/otp/migrations/0001_initial.py b/passbook/stages/otp/migrations/0001_initial.py
similarity index 67%
rename from passbook/factors/otp/migrations/0001_initial.py
rename to passbook/stages/otp/migrations/0001_initial.py
index fe06e9300..205c8c98b 100644
--- a/passbook/factors/otp/migrations/0001_initial.py
+++ b/passbook/stages/otp/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.6 on 2019-10-07 14:07
+# Generated by Django 3.0.3 on 2020-05-08 17:59
import django.db.models.deletion
from django.db import migrations, models
@@ -9,36 +9,33 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ("passbook_core", "0001_initial"),
+ ("passbook_flows", "0001_initial"),
]
operations = [
migrations.CreateModel(
- name="OTPFactor",
+ name="OTPStage",
fields=[
(
- "factor_ptr",
+ "stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
- to="passbook_core.Factor",
+ to="passbook_flows.Stage",
),
),
(
"enforced",
models.BooleanField(
default=False,
- help_text="Enforce enabled OTP for Users this factor applies to.",
+ help_text="Enforce enabled OTP for Users this stage applies to.",
),
),
],
- options={
- "verbose_name": "OTP Factor",
- "verbose_name_plural": "OTP Factors",
- },
- bases=("passbook_core.factor",),
+ options={"verbose_name": "OTP Stage", "verbose_name_plural": "OTP Stages",},
+ bases=("passbook_flows.stage",),
),
]
diff --git a/passbook/factors/otp/migrations/__init__.py b/passbook/stages/otp/migrations/__init__.py
similarity index 100%
rename from passbook/factors/otp/migrations/__init__.py
rename to passbook/stages/otp/migrations/__init__.py
diff --git a/passbook/stages/otp/models.py b/passbook/stages/otp/models.py
new file mode 100644
index 000000000..f0a6e1c88
--- /dev/null
+++ b/passbook/stages/otp/models.py
@@ -0,0 +1,34 @@
+"""OTP Stage"""
+from django.db import models
+from django.utils.translation import gettext as _
+
+from passbook.core.types import UIUserSettings
+from passbook.flows.models import Stage
+
+
+class OTPStage(Stage):
+ """OTP Stage"""
+
+ enforced = models.BooleanField(
+ default=False,
+ help_text=("Enforce enabled OTP for Users " "this stage applies to."),
+ )
+
+ type = "passbook.stages.otp.stages.OTPStage"
+ form = "passbook.stages.otp.forms.OTPStageForm"
+
+ @property
+ def ui_user_settings(self) -> UIUserSettings:
+ return UIUserSettings(
+ name="OTP",
+ icon="pficon-locked",
+ view_name="passbook_stages_otp:otp-user-settings",
+ )
+
+ def __str__(self):
+ return f"OTP Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("OTP Stage")
+ verbose_name_plural = _("OTP Stages")
diff --git a/passbook/factors/otp/settings.py b/passbook/stages/otp/settings.py
similarity index 100%
rename from passbook/factors/otp/settings.py
rename to passbook/stages/otp/settings.py
diff --git a/passbook/factors/otp/factors.py b/passbook/stages/otp/stage.py
similarity index 82%
rename from passbook/factors/otp/factors.py
rename to passbook/stages/otp/stage.py
index 50ba3d21f..3d62f7519 100644
--- a/passbook/factors/otp/factors.py
+++ b/passbook/stages/otp/stage.py
@@ -1,22 +1,22 @@
-"""OTP Factor logic"""
+"""OTP Stage logic"""
from django.contrib import messages
from django.utils.translation import gettext as _
from django.views.generic import FormView
from django_otp import match_token, user_has_device
from structlog import get_logger
-from passbook.factors.otp.forms import OTPVerifyForm
-from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView
-from passbook.flows.factor_base import AuthenticationFactor
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
+from passbook.flows.stage import AuthenticationStage
+from passbook.stages.otp.forms import OTPVerifyForm
+from passbook.stages.otp.views import OTP_SETTING_UP_KEY, EnableView
LOGGER = get_logger()
-class OTPFactor(FormView, AuthenticationFactor):
- """OTP Factor View"""
+class OTPStage(FormView, AuthenticationStage):
+ """OTP Stage View"""
- template_name = "otp/factor.html"
+ template_name = "stages/otp/stage.html"
form_class = OTPVerifyForm
def get_context_data(self, **kwargs):
@@ -29,7 +29,7 @@ class OTPFactor(FormView, AuthenticationFactor):
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
if not user_has_device(pending_user):
LOGGER.debug("User doesn't have OTP Setup.")
- if self.executor.current_factor.enforced:
+ if self.executor.current_stage.enforced:
# Redirect to setup view
LOGGER.debug("OTP is enforced, redirecting to setup")
request.user = pending_user
@@ -54,6 +54,6 @@ class OTPFactor(FormView, AuthenticationFactor):
form.cleaned_data.get("code"),
)
if device:
- return self.executor.factor_ok()
+ return self.executor.stage_ok()
messages.error(self.request, _("Invalid OTP."))
return self.form_invalid(form)
diff --git a/passbook/factors/otp/templates/otp/factor.html b/passbook/stages/otp/templates/stages/otp/factor.html
similarity index 100%
rename from passbook/factors/otp/templates/otp/factor.html
rename to passbook/stages/otp/templates/stages/otp/factor.html
diff --git a/passbook/factors/otp/templates/otp/user_settings.html b/passbook/stages/otp/templates/stages/otp/user_settings.html
similarity index 91%
rename from passbook/factors/otp/templates/otp/user_settings.html
rename to passbook/stages/otp/templates/stages/otp/user_settings.html
index 4764de835..bb5ff5359 100644
--- a/passbook/factors/otp/templates/otp/user_settings.html
+++ b/passbook/stages/otp/templates/stages/otp/user_settings.html
@@ -23,10 +23,10 @@
{% if not state %}
- {% trans "Enable OTP" %}
{% else %}
- {% trans "Disable OTP" %}
{% endif %}
diff --git a/passbook/factors/otp/urls.py b/passbook/stages/otp/urls.py
similarity index 89%
rename from passbook/factors/otp/urls.py
rename to passbook/stages/otp/urls.py
index 4efc3ca88..012ff2923 100644
--- a/passbook/factors/otp/urls.py
+++ b/passbook/stages/otp/urls.py
@@ -2,7 +2,7 @@
from django.urls import path
-from passbook.factors.otp import views
+from passbook.stages.otp import views
urlpatterns = [
path("", views.UserSettingsView.as_view(), name="otp-user-settings"),
diff --git a/passbook/factors/otp/utils.py b/passbook/stages/otp/utils.py
similarity index 100%
rename from passbook/factors/otp/utils.py
rename to passbook/stages/otp/utils.py
diff --git a/passbook/factors/otp/views.py b/passbook/stages/otp/views.py
similarity index 91%
rename from passbook/factors/otp/views.py
rename to passbook/stages/otp/views.py
index 5561482ce..833441318 100644
--- a/passbook/factors/otp/views.py
+++ b/passbook/stages/otp/views.py
@@ -19,12 +19,12 @@ from qrcode.image.svg import SvgPathImage
from structlog import get_logger
from passbook.audit.models import Event, EventAction
-from passbook.factors.otp.forms import OTPSetupForm
-from passbook.factors.otp.utils import otpauth_url
from passbook.lib.config import CONFIG
+from passbook.stages.otp.forms import OTPSetupForm
+from passbook.stages.otp.utils import otpauth_url
-OTP_SESSION_KEY = "passbook_factors_otp_key"
-OTP_SETTING_UP_KEY = "passbook_factors_otp_setup"
+OTP_SESSION_KEY = "passbook_stages_otp_key"
+OTP_SETTING_UP_KEY = "passbook_stages_otp_setup"
LOGGER = get_logger()
@@ -33,7 +33,7 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
template_name = "otp/user_settings.html"
- # TODO: Check if OTP Factor exists and applies to user
+ # TODO: Check if OTP Stage exists and applies to user
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
static = StaticDevice.objects.filter(user=self.request.user, confirmed=True)
@@ -61,7 +61,7 @@ class DisableView(LoginRequiredMixin, View):
messages.success(request, "Successfully disabled OTP")
# Create event with email notification
Event.new(EventAction.CUSTOM, message="User disabled OTP.").from_http(request)
- return redirect(reverse("passbook_factors_otp:otp-user-settings"))
+ return redirect(reverse("passbook_stages_otp:otp-user-settings"))
class EnableView(LoginRequiredMixin, FormView):
@@ -74,7 +74,7 @@ class EnableView(LoginRequiredMixin, FormView):
totp_device = None
static_device = None
- # TODO: Check if OTP Factor exists and applies to user
+ # TODO: Check if OTP Stage exists and applies to user
def get_context_data(self, **kwargs):
kwargs["config"] = CONFIG.y("passbook")
kwargs["title"] = _("Configure OTP")
@@ -92,7 +92,7 @@ class EnableView(LoginRequiredMixin, FormView):
if finished_totp_devices.exists() and finished_static_devices.exists():
messages.error(request, _("You already have TOTP enabled!"))
del request.session[OTP_SETTING_UP_KEY]
- return redirect("passbook_factors_otp:otp-user-settings")
+ return redirect("passbook_stages_otp:otp-user-settings")
request.session[OTP_SETTING_UP_KEY] = True
# Check if there's an unconfirmed device left to set up
totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
@@ -127,7 +127,7 @@ class EnableView(LoginRequiredMixin, FormView):
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
form.device = self.totp_device
- form.fields["qr_code"].initial = reverse("passbook_factors_otp:otp-qr")
+ form.fields["qr_code"].initial = reverse("passbook_stages_otp:otp-qr")
tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
form.fields["tokens"].choices = tokens
return form
@@ -143,7 +143,7 @@ class EnableView(LoginRequiredMixin, FormView):
Event.new(EventAction.CUSTOM, message="User enabled OTP.").from_http(
self.request
)
- return redirect("passbook_factors_otp:otp-user-settings")
+ return redirect("passbook_stages_otp:otp-user-settings")
@method_decorator(never_cache, name="dispatch")
diff --git a/passbook/factors/password/__init__.py b/passbook/stages/password/__init__.py
similarity index 100%
rename from passbook/factors/password/__init__.py
rename to passbook/stages/password/__init__.py
diff --git a/passbook/stages/password/api.py b/passbook/stages/password/api.py
new file mode 100644
index 000000000..48e1c5191
--- /dev/null
+++ b/passbook/stages/password/api.py
@@ -0,0 +1,26 @@
+"""PasswordStage API Views"""
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import ModelViewSet
+
+from passbook.stages.password.models import PasswordStage
+
+
+class PasswordStageSerializer(ModelSerializer):
+ """PasswordStage Serializer"""
+
+ class Meta:
+
+ model = PasswordStage
+ fields = [
+ "pk",
+ "name",
+ "backends",
+ "password_policies",
+ ]
+
+
+class PasswordStageViewSet(ModelViewSet):
+ """PasswordStage Viewset"""
+
+ queryset = PasswordStage.objects.all()
+ serializer_class = PasswordStageSerializer
diff --git a/passbook/stages/password/apps.py b/passbook/stages/password/apps.py
new file mode 100644
index 000000000..087e1f90c
--- /dev/null
+++ b/passbook/stages/password/apps.py
@@ -0,0 +1,10 @@
+"""passbook core app config"""
+from django.apps import AppConfig
+
+
+class PassbookStagePasswordConfig(AppConfig):
+ """passbook password stage config"""
+
+ name = "passbook.stages.password"
+ label = "passbook_stages_password"
+ verbose_name = "passbook Stages.Password"
diff --git a/passbook/factors/password/exceptions.py b/passbook/stages/password/exceptions.py
similarity index 100%
rename from passbook/factors/password/exceptions.py
rename to passbook/stages/password/exceptions.py
diff --git a/passbook/factors/password/forms.py b/passbook/stages/password/forms.py
similarity index 64%
rename from passbook/factors/password/forms.py
rename to passbook/stages/password/forms.py
index 50b48e5c0..f9dc91421 100644
--- a/passbook/factors/password/forms.py
+++ b/passbook/stages/password/forms.py
@@ -4,9 +4,8 @@ from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
-from passbook.factors.password.models import PasswordFactor
-from passbook.flows.forms import GENERAL_FIELDS
from passbook.lib.utils.reflection import path_to_class
+from passbook.stages.password.models import PasswordStage
def get_authentication_backends():
@@ -32,25 +31,17 @@ class PasswordForm(forms.Form):
)
-class PasswordFactorForm(forms.ModelForm):
- """Form to create/edit Password Factors"""
+class PasswordStageForm(forms.ModelForm):
+ """Form to create/edit Password Stages"""
class Meta:
- model = PasswordFactor
- fields = GENERAL_FIELDS + ["backends", "password_policies", "reset_factors"]
+ model = PasswordStage
+ fields = ["name", "backends"]
widgets = {
"name": forms.TextInput(),
- "order": forms.NumberInput(),
- "policies": FilteredSelectMultiple(_("policies"), False),
"backends": FilteredSelectMultiple(
_("backends"), False, choices=get_authentication_backends()
),
"password_policies": FilteredSelectMultiple(_("password policies"), False),
- "reset_factors": FilteredSelectMultiple(_("reset factors"), False),
- }
- help_texts = {
- "policies": _(
- "Policies which determine if this factor applies to the current user."
- )
}
diff --git a/passbook/factors/password/migrations/0001_initial.py b/passbook/stages/password/migrations/0001_initial.py
similarity index 62%
rename from passbook/factors/password/migrations/0001_initial.py
rename to passbook/stages/password/migrations/0001_initial.py
index 58be06b49..b2c740c1d 100644
--- a/passbook/factors/password/migrations/0001_initial.py
+++ b/passbook/stages/password/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.6 on 2019-10-07 14:07
+# Generated by Django 3.0.3 on 2020-05-08 17:58
import django.contrib.postgres.fields
import django.db.models.deletion
@@ -10,28 +10,31 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ("passbook_core", "0001_initial"),
+ ("passbook_flows", "0001_initial"),
+ ("passbook_core", "0012_delete_factor"),
]
operations = [
migrations.CreateModel(
- name="PasswordFactor",
+ name="PasswordStage",
fields=[
(
- "factor_ptr",
+ "stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
- to="passbook_core.Factor",
+ to="passbook_flows.Stage",
),
),
(
"backends",
django.contrib.postgres.fields.ArrayField(
- base_field=models.TextField(), size=None
+ base_field=models.TextField(),
+ help_text="Selection of backends to test the password against.",
+ size=None,
),
),
(
@@ -40,9 +43,9 @@ class Migration(migrations.Migration):
),
],
options={
- "verbose_name": "Password Factor",
- "verbose_name_plural": "Password Factors",
+ "verbose_name": "Password Stage",
+ "verbose_name_plural": "Password Stages",
},
- bases=("passbook_core.factor",),
+ bases=("passbook_flows.stage",),
),
]
diff --git a/passbook/factors/password/migrations/__init__.py b/passbook/stages/password/migrations/__init__.py
similarity index 100%
rename from passbook/factors/password/migrations/__init__.py
rename to passbook/stages/password/migrations/__init__.py
diff --git a/passbook/factors/password/models.py b/passbook/stages/password/models.py
similarity index 63%
rename from passbook/factors/password/models.py
rename to passbook/stages/password/models.py
index 9247f80be..74359a876 100644
--- a/passbook/factors/password/models.py
+++ b/passbook/stages/password/models.py
@@ -1,26 +1,24 @@
-"""password factor models"""
+"""password stage models"""
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
-from passbook.core.models import Factor, Policy, User
+from passbook.core.models import Policy, User
from passbook.core.types import UIUserSettings
+from passbook.flows.models import Stage
-class PasswordFactor(Factor):
- """Password-based Django-backend Authentication Factor"""
+class PasswordStage(Stage):
+ """Password-based Django-backend Authentication Stage"""
backends = ArrayField(
models.TextField(),
help_text=_("Selection of backends to test the password against."),
)
password_policies = models.ManyToManyField(Policy, blank=True)
- reset_factors = models.ManyToManyField(
- Factor, blank=True, related_name="reset_factors"
- )
- type = "passbook.factors.password.factor.PasswordFactor"
- form = "passbook.factors.password.forms.PasswordFactorForm"
+ type = "passbook.stages.password.stage.PasswordStage"
+ form = "passbook.stages.password.forms.PasswordStageForm"
@property
def ui_user_settings(self) -> UIUserSettings:
@@ -38,9 +36,9 @@ class PasswordFactor(Factor):
return True
def __str__(self):
- return "Password Factor %s" % self.slug
+ return f"Password Stage {self.name}"
class Meta:
- verbose_name = _("Password Factor")
- verbose_name_plural = _("Password Factors")
+ verbose_name = _("Password Stage")
+ verbose_name_plural = _("Password Stages")
diff --git a/passbook/factors/password/factor.py b/passbook/stages/password/stage.py
similarity index 86%
rename from passbook/factors/password/factor.py
rename to passbook/stages/password/stage.py
index ce048b820..d3e691dd7 100644
--- a/passbook/factors/password/factor.py
+++ b/passbook/stages/password/stage.py
@@ -1,4 +1,4 @@
-"""passbook multi-factor authentication engine"""
+"""passbook password stage"""
from inspect import Signature
from typing import Optional
@@ -11,11 +11,11 @@ from django.views.generic import FormView
from structlog import get_logger
from passbook.core.models import User
-from passbook.factors.password.forms import PasswordForm
-from passbook.flows.factor_base import AuthenticationFactor
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
+from passbook.flows.stage import AuthenticationStage
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class
+from passbook.stages.password.forms import PasswordForm
LOGGER = get_logger()
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
@@ -53,11 +53,11 @@ def authenticate(request, backends, **credentials) -> Optional[User]:
)
-class PasswordFactor(FormView, AuthenticationFactor):
- """Authentication factor which authenticates against django's AuthBackend"""
+class PasswordStage(FormView, AuthenticationStage):
+ """Authentication stage which authenticates against django's AuthBackend"""
form_class = PasswordForm
- template_name = "factors/password/backend.html"
+ template_name = "stages/password/backend.html"
def form_valid(self, form):
"""Authenticate against django's authentication backend"""
@@ -71,7 +71,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
)
try:
user = authenticate(
- self.request, self.executor.current_factor.backends, **kwargs
+ self.request, self.executor.current_stage.backends, **kwargs
)
if user:
# User instance returned from authenticate() has .backend property set
@@ -79,7 +79,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
self.executor.plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
] = user.backend
- return self.executor.factor_ok()
+ return self.executor.stage_ok()
# No user was found -> invalid credentials
LOGGER.debug("Invalid credentials")
# Manually inject error into form
@@ -90,4 +90,4 @@ class PasswordFactor(FormView, AuthenticationFactor):
except PermissionDenied:
# User was found, but permission was denied (i.e. user is not active)
LOGGER.debug("Denied access", **kwargs)
- return self.executor.factor_invalid()
+ return self.executor.stage_invalid()
diff --git a/passbook/factors/password/templates/factors/password/backend.html b/passbook/stages/password/templates/stages/password/backend.html
similarity index 100%
rename from passbook/factors/password/templates/factors/password/backend.html
rename to passbook/stages/password/templates/stages/password/backend.html
diff --git a/scripts/coverage.sh b/scripts/coverage.sh
index 61f10d820..c95d47574 100755
--- a/scripts/coverage.sh
+++ b/scripts/coverage.sh
@@ -1,5 +1,5 @@
#!/bin/bash -xe
-coverage run --concurrency=multiprocessing manage.py test
+coverage run --concurrency=multiprocessing manage.py test --failfast
coverage combine
coverage html
coverage report