factors: -> stage
This commit is contained in:
		@ -6,7 +6,6 @@ from passbook.admin.views import (
 | 
				
			|||||||
    audit,
 | 
					    audit,
 | 
				
			||||||
    certificate_key_pair,
 | 
					    certificate_key_pair,
 | 
				
			||||||
    debug,
 | 
					    debug,
 | 
				
			||||||
    factors,
 | 
					 | 
				
			||||||
    flows,
 | 
					    flows,
 | 
				
			||||||
    groups,
 | 
					    groups,
 | 
				
			||||||
    invitations,
 | 
					    invitations,
 | 
				
			||||||
@ -15,6 +14,7 @@ from passbook.admin.views import (
 | 
				
			|||||||
    property_mapping,
 | 
					    property_mapping,
 | 
				
			||||||
    providers,
 | 
					    providers,
 | 
				
			||||||
    sources,
 | 
					    sources,
 | 
				
			||||||
 | 
					    stages,
 | 
				
			||||||
    users,
 | 
					    users,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -85,18 +85,18 @@ urlpatterns = [
 | 
				
			|||||||
        providers.ProviderDeleteView.as_view(),
 | 
					        providers.ProviderDeleteView.as_view(),
 | 
				
			||||||
        name="provider-delete",
 | 
					        name="provider-delete",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    # Factors
 | 
					    # Stages
 | 
				
			||||||
    path("factors/", factors.FactorListView.as_view(), name="factors"),
 | 
					    path("stages/", stages.StageListView.as_view(), name="stages"),
 | 
				
			||||||
    path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"),
 | 
					    path("stages/create/", stages.StageCreateView.as_view(), name="stage-create"),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "factors/<uuid:pk>/update/",
 | 
					        "stages/<uuid:pk>/update/",
 | 
				
			||||||
        factors.FactorUpdateView.as_view(),
 | 
					        stages.StageUpdateView.as_view(),
 | 
				
			||||||
        name="factor-update",
 | 
					        name="stage-update",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "factors/<uuid:pk>/delete/",
 | 
					        "stages/<uuid:pk>/delete/",
 | 
				
			||||||
        factors.FactorDeleteView.as_view(),
 | 
					        stages.StageDeleteView.as_view(),
 | 
				
			||||||
        name="factor-delete",
 | 
					        name="stage-delete",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    # Flows
 | 
					    # Flows
 | 
				
			||||||
    path("flows/", flows.FlowListView.as_view(), name="flows"),
 | 
					    path("flows/", flows.FlowListView.as_view(), name="flows"),
 | 
				
			||||||
@ -107,7 +107,7 @@ urlpatterns = [
 | 
				
			|||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
 | 
					        "flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    # Factors
 | 
					    # Property Mappings
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "property-mappings/",
 | 
					        "property-mappings/",
 | 
				
			||||||
        property_mapping.PropertyMappingListView.as_view(),
 | 
					        property_mapping.PropertyMappingListView.as_view(),
 | 
				
			||||||
 | 
				
			|||||||
@ -5,15 +5,8 @@ from django.views.generic import TemplateView
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from passbook import __version__
 | 
					from passbook import __version__
 | 
				
			||||||
from passbook.admin.mixins import AdminRequiredMixin
 | 
					from passbook.admin.mixins import AdminRequiredMixin
 | 
				
			||||||
from passbook.core.models import (
 | 
					from passbook.core.models import Application, Invitation, Policy, Provider, Source, User
 | 
				
			||||||
    Application,
 | 
					from passbook.flows.models import Flow, Stage
 | 
				
			||||||
    Factor,
 | 
					 | 
				
			||||||
    Invitation,
 | 
					 | 
				
			||||||
    Policy,
 | 
					 | 
				
			||||||
    Provider,
 | 
					 | 
				
			||||||
    Source,
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from passbook.root.celery import CELERY_APP
 | 
					from passbook.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,7 +28,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
				
			|||||||
        kwargs["user_count"] = len(User.objects.all())
 | 
					        kwargs["user_count"] = len(User.objects.all())
 | 
				
			||||||
        kwargs["provider_count"] = len(Provider.objects.all())
 | 
					        kwargs["provider_count"] = len(Provider.objects.all())
 | 
				
			||||||
        kwargs["source_count"] = len(Source.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["invitation_count"] = len(Invitation.objects.all())
 | 
				
			||||||
        kwargs["version"] = __version__
 | 
					        kwargs["version"] = __version__
 | 
				
			||||||
        kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
 | 
					        kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""passbook Factor administration"""
 | 
					"""passbook Stage administration"""
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
from django.contrib.auth.mixins import (
 | 
					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 django.views.generic import DeleteView, ListView, UpdateView
 | 
				
			||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
					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.utils.reflection import path_to_class
 | 
				
			||||||
from passbook.lib.views import CreateAssignPermView
 | 
					from passbook.lib.views import CreateAssignPermView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,18 +23,18 @@ def all_subclasses(cls):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
					class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
				
			||||||
    """Show list of all factors"""
 | 
					    """Show list of all flows"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Factor
 | 
					    model = Stage
 | 
				
			||||||
    template_name = "administration/factor/list.html"
 | 
					    template_name = "administration/flow/list.html"
 | 
				
			||||||
    permission_required = "passbook_core.view_factor"
 | 
					    permission_required = "passbook_core.view_flow"
 | 
				
			||||||
    ordering = "order"
 | 
					    ordering = "order"
 | 
				
			||||||
    paginate_by = 40
 | 
					    paginate_by = 40
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs["types"] = {
 | 
					        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)
 | 
					        return super().get_context_data(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -42,46 +42,46 @@ class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
				
			|||||||
        return super().get_queryset().select_subclasses()
 | 
					        return super().get_queryset().select_subclasses()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FactorCreateView(
 | 
					class StageCreateView(
 | 
				
			||||||
    SuccessMessageMixin,
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
    LoginRequiredMixin,
 | 
					    LoginRequiredMixin,
 | 
				
			||||||
    DjangoPermissionRequiredMixin,
 | 
					    DjangoPermissionRequiredMixin,
 | 
				
			||||||
    CreateAssignPermView,
 | 
					    CreateAssignPermView,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Create new Factor"""
 | 
					    """Create new Stage"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Factor
 | 
					    model = Stage
 | 
				
			||||||
    template_name = "generic/create.html"
 | 
					    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_url = reverse_lazy("passbook_admin:flows")
 | 
				
			||||||
    success_message = _("Successfully created Factor")
 | 
					    success_message = _("Successfully created Stage")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        factor_type = self.request.GET.get("type")
 | 
					        flow_type = self.request.GET.get("type")
 | 
				
			||||||
        model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
 | 
					        model = next(x for x in all_subclasses(Stage) if x.__name__ == flow_type)
 | 
				
			||||||
        kwargs["type"] = model._meta.verbose_name
 | 
					        kwargs["type"] = model._meta.verbose_name
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_form_class(self):
 | 
					    def get_form_class(self):
 | 
				
			||||||
        factor_type = self.request.GET.get("type")
 | 
					        flow_type = self.request.GET.get("type")
 | 
				
			||||||
        model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
 | 
					        model = next(x for x in all_subclasses(Stage) if x.__name__ == flow_type)
 | 
				
			||||||
        if not model:
 | 
					        if not model:
 | 
				
			||||||
            raise Http404
 | 
					            raise Http404
 | 
				
			||||||
        return path_to_class(model.form)
 | 
					        return path_to_class(model.form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FactorUpdateView(
 | 
					class StageUpdateView(
 | 
				
			||||||
    SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
 | 
					    SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Update factor"""
 | 
					    """Update flow"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Factor
 | 
					    model = Stage
 | 
				
			||||||
    permission_required = "passbook_core.update_application"
 | 
					    permission_required = "passbook_core.update_application"
 | 
				
			||||||
    template_name = "generic/update.html"
 | 
					    template_name = "generic/update.html"
 | 
				
			||||||
    success_url = reverse_lazy("passbook_admin:factors")
 | 
					    success_url = reverse_lazy("passbook_admin:flows")
 | 
				
			||||||
    success_message = _("Successfully updated Factor")
 | 
					    success_message = _("Successfully updated Stage")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_form_class(self):
 | 
					    def get_form_class(self):
 | 
				
			||||||
        form_class_path = self.get_object().form
 | 
					        form_class_path = self.get_object().form
 | 
				
			||||||
@ -90,24 +90,24 @@ class FactorUpdateView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_object(self, queryset=None):
 | 
					    def get_object(self, queryset=None):
 | 
				
			||||||
        return (
 | 
					        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
 | 
					    SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Delete factor"""
 | 
					    """Delete flow"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Factor
 | 
					    model = Stage
 | 
				
			||||||
    template_name = "generic/delete.html"
 | 
					    template_name = "generic/delete.html"
 | 
				
			||||||
    permission_required = "passbook_core.delete_factor"
 | 
					    permission_required = "passbook_core.delete_flow"
 | 
				
			||||||
    success_url = reverse_lazy("passbook_admin:factors")
 | 
					    success_url = reverse_lazy("passbook_admin:flows")
 | 
				
			||||||
    success_message = _("Successfully deleted Factor")
 | 
					    success_message = _("Successfully deleted Stage")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object(self, queryset=None):
 | 
					    def get_object(self, queryset=None):
 | 
				
			||||||
        return (
 | 
					        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):
 | 
					    def delete(self, request, *args, **kwargs):
 | 
				
			||||||
@ -9,7 +9,6 @@ from structlog import get_logger
 | 
				
			|||||||
from passbook.api.permissions import CustomObjectPermissions
 | 
					from passbook.api.permissions import CustomObjectPermissions
 | 
				
			||||||
from passbook.audit.api import EventViewSet
 | 
					from passbook.audit.api import EventViewSet
 | 
				
			||||||
from passbook.core.api.applications import ApplicationViewSet
 | 
					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.groups import GroupViewSet
 | 
				
			||||||
from passbook.core.api.invitations import InvitationViewSet
 | 
					from passbook.core.api.invitations import InvitationViewSet
 | 
				
			||||||
from passbook.core.api.policies import PolicyViewSet
 | 
					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.providers import ProviderViewSet
 | 
				
			||||||
from passbook.core.api.sources import SourceViewSet
 | 
					from passbook.core.api.sources import SourceViewSet
 | 
				
			||||||
from passbook.core.api.users import UserViewSet
 | 
					from passbook.core.api.users import UserViewSet
 | 
				
			||||||
from passbook.factors.captcha.api import CaptchaFactorViewSet
 | 
					from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
 | 
				
			||||||
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.lib.utils.reflection import get_apps
 | 
					from passbook.lib.utils.reflection import get_apps
 | 
				
			||||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
					from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
				
			||||||
from passbook.policies.expression.api import ExpressionPolicyViewSet
 | 
					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.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
 | 
				
			||||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
 | 
					from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
 | 
				
			||||||
from passbook.sources.oauth.api import OAuthSourceViewSet
 | 
					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()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
router = routers.DefaultRouter()
 | 
					router = routers.DefaultRouter()
 | 
				
			||||||
@ -69,14 +68,14 @@ router.register("providers/saml", SAMLProviderViewSet)
 | 
				
			|||||||
router.register("propertymappings/all", PropertyMappingViewSet)
 | 
					router.register("propertymappings/all", PropertyMappingViewSet)
 | 
				
			||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
 | 
					router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
 | 
				
			||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
 | 
					router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
 | 
				
			||||||
router.register("factors/all", FactorViewSet)
 | 
					router.register("stages/all", StageViewSet)
 | 
				
			||||||
router.register("factors/captcha", CaptchaFactorViewSet)
 | 
					router.register("stages/captcha", CaptchaStageViewSet)
 | 
				
			||||||
router.register("factors/dummy", DummyFactorViewSet)
 | 
					router.register("stages/dummy", DummyStageViewSet)
 | 
				
			||||||
router.register("factors/email", EmailFactorViewSet)
 | 
					router.register("stages/email", EmailStageViewSet)
 | 
				
			||||||
router.register("factors/otp", OTPFactorViewSet)
 | 
					router.register("stages/otp", OTPStageViewSet)
 | 
				
			||||||
router.register("factors/password", PasswordFactorViewSet)
 | 
					router.register("stages/password", PasswordStageViewSet)
 | 
				
			||||||
router.register("flows", FlowViewSet)
 | 
					router.register("flows", FlowViewSet)
 | 
				
			||||||
router.register("flows/bindings", FlowFactorBindingViewSet)
 | 
					router.register("flows/bindings", FlowStageBindingViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
info = openapi.Info(
 | 
					info = openapi.Info(
 | 
				
			||||||
    title="passbook API",
 | 
					    title="passbook API",
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
					 | 
				
			||||||
							
								
								
									
										14
									
								
								passbook/core/migrations/0012_delete_factor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								passbook/core/migrations/0012_delete_factor.py
									
									
									
									
									
										Normal file
									
								
							@ -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",),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -103,30 +103,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
 | 
				
			|||||||
    policies = models.ManyToManyField("Policy", blank=True)
 | 
					    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):
 | 
					class Application(ExportModelOperationsMixin("application"), PolicyModel):
 | 
				
			||||||
    """Every Application which uses passbook for authentication/identification/authorization
 | 
					    """Every Application which uses passbook for authentication/identification/authorization
 | 
				
			||||||
    needs an Application record. Other authentication types can subclass this Model to
 | 
					    needs an Application record. Other authentication types can subclass this Model to
 | 
				
			||||||
 | 
				
			|||||||
@ -18,16 +18,16 @@
 | 
				
			|||||||
                    </li>
 | 
					                    </li>
 | 
				
			||||||
                </ul>
 | 
					                </ul>
 | 
				
			||||||
            </section>
 | 
					            </section>
 | 
				
			||||||
            {% user_factors as user_factors_loc %}
 | 
					            {% user_stages as user_stages_loc %}
 | 
				
			||||||
            {% if user_factors_loc %}
 | 
					            {% if user_stages_loc %}
 | 
				
			||||||
            <section class="pf-c-nav__section">
 | 
					            <section class="pf-c-nav__section">
 | 
				
			||||||
                <h2 class="pf-c-nav__section-title">{% trans 'Factors' %}</h2>
 | 
					                <h2 class="pf-c-nav__section-title">{% trans 'Factors' %}</h2>
 | 
				
			||||||
                <ul class="pf-c-nav__list">
 | 
					                <ul class="pf-c-nav__list">
 | 
				
			||||||
                    {% for factor in user_factors_loc %}
 | 
					                    {% for stage in user_stages_loc %}
 | 
				
			||||||
                    <li class="pf-c-nav__item">
 | 
					                    <li class="pf-c-nav__item">
 | 
				
			||||||
                        <a href="{% url factor.view_name %}" class="pf-c-nav__link {% is_active factor.view_name %}">
 | 
					                        <a href="{% url stage.view_name %}" class="pf-c-nav__link {% is_active stage.view_name %}">
 | 
				
			||||||
                            <i class="{{ factor.icon }}"></i>
 | 
					                            <i class="{{ stage.icon }}"></i>
 | 
				
			||||||
                            {{ factor.name }}
 | 
					                            {{ stage.name }}
 | 
				
			||||||
                        </a>
 | 
					                        </a>
 | 
				
			||||||
                    </li>
 | 
					                    </li>
 | 
				
			||||||
                    {% endfor %}
 | 
					                    {% endfor %}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from typing import Iterable, List
 | 
				
			|||||||
from django import template
 | 
					from django import template
 | 
				
			||||||
from django.template.context import RequestContext
 | 
					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.core.types import UIUserSettings
 | 
				
			||||||
from passbook.policies.engine import PolicyEngine
 | 
					from passbook.policies.engine import PolicyEngine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,24 +12,24 @@ register = template.Library()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(takes_context=True)
 | 
					@register.simple_tag(takes_context=True)
 | 
				
			||||||
def user_factors(context: RequestContext) -> List[UIUserSettings]:
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
    """Return list of all factors which apply to user"""
 | 
					def user_stages(context: RequestContext) -> List[UIUserSettings]:
 | 
				
			||||||
    user = context.get("request").user
 | 
					    """Return list of all stages which apply to user"""
 | 
				
			||||||
    _all_factors: Iterable[Factor] = (
 | 
					    # TODO: Rewrite this based on flows
 | 
				
			||||||
        Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
 | 
					    # user = context.get("request").user
 | 
				
			||||||
    )
 | 
					    # _all_stages: Iterable[Stage] = (Stage.objects.all().select_subclasses())
 | 
				
			||||||
    matching_factors: List[UIUserSettings] = []
 | 
					    matching_stages: List[UIUserSettings] = []
 | 
				
			||||||
    for factor in _all_factors:
 | 
					    # for stage in _all_stages:
 | 
				
			||||||
        user_settings = factor.ui_user_settings
 | 
					    #     user_settings = stage.ui_user_settings
 | 
				
			||||||
        if not user_settings:
 | 
					    #     if not user_settings:
 | 
				
			||||||
            continue
 | 
					    #         continue
 | 
				
			||||||
        policy_engine = PolicyEngine(
 | 
					    #     policy_engine = PolicyEngine(
 | 
				
			||||||
            factor.policies.all(), user, context.get("request")
 | 
					    #         stage.policies.all(), user, context.get("request")
 | 
				
			||||||
        )
 | 
					    #     )
 | 
				
			||||||
        policy_engine.build()
 | 
					    #     policy_engine.build()
 | 
				
			||||||
        if policy_engine.passing:
 | 
					    #     if policy_engine.passing:
 | 
				
			||||||
            matching_factors.append(user_settings)
 | 
					    #         matching_stages.append(user_settings)
 | 
				
			||||||
    return matching_factors
 | 
					    return matching_stages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(takes_context=True)
 | 
					@register.simple_tag(takes_context=True)
 | 
				
			||||||
@ -40,12 +40,12 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
 | 
				
			|||||||
        Source.objects.filter(enabled=True).select_subclasses()
 | 
					        Source.objects.filter(enabled=True).select_subclasses()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    matching_sources: List[UIUserSettings] = []
 | 
					    matching_sources: List[UIUserSettings] = []
 | 
				
			||||||
    for factor in _all_sources:
 | 
					    for source in _all_sources:
 | 
				
			||||||
        user_settings = factor.ui_user_settings
 | 
					        user_settings = source.ui_user_settings
 | 
				
			||||||
        if not user_settings:
 | 
					        if not user_settings:
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
        policy_engine = PolicyEngine(
 | 
					        policy_engine = PolicyEngine(
 | 
				
			||||||
            factor.policies.all(), user, context.get("request")
 | 
					            source.policies.all(), user, context.get("request")
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        policy_engine.build()
 | 
					        policy_engine.build()
 | 
				
			||||||
        if policy_engine.passing:
 | 
					        if policy_engine.passing:
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ from typing import Optional
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class UIUserSettings:
 | 
					class UIUserSettings:
 | 
				
			||||||
    """Dataclass for Factor and Source's user_settings"""
 | 
					    """Dataclass for Stage and Source's user_settings"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name: str
 | 
					    name: str
 | 
				
			||||||
    icon: str
 | 
					    icon: str
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,12 @@ from structlog import get_logger
 | 
				
			|||||||
from passbook.core.forms.authentication import LoginForm, SignUpForm
 | 
					from passbook.core.forms.authentication import LoginForm, SignUpForm
 | 
				
			||||||
from passbook.core.models import Invitation, Nonce, Source, User
 | 
					from passbook.core.models import Invitation, Nonce, Source, User
 | 
				
			||||||
from passbook.core.signals import invitation_used, user_signed_up
 | 
					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.models import Flow, FlowDesignation
 | 
				
			||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
					from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
				
			||||||
from passbook.flows.views import SESSION_KEY_PLAN
 | 
					from passbook.flows.views import SESSION_KEY_PLAN
 | 
				
			||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
from passbook.lib.utils.urls import redirect_with_qs
 | 
					from passbook.lib.utils.urls import redirect_with_qs
 | 
				
			||||||
 | 
					from passbook.stages.password.exceptions import PasswordPolicyInvalid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,8 +10,8 @@ from django.utils.translation import gettext as _
 | 
				
			|||||||
from django.views.generic import DeleteView, FormView, UpdateView
 | 
					from django.views.generic import DeleteView, FormView, UpdateView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
 | 
					from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
 | 
				
			||||||
from passbook.factors.password.exceptions import PasswordPolicyInvalid
 | 
					 | 
				
			||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
 | 
					from passbook.stages.password.exceptions import PasswordPolicyInvalid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
 | 
					class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					 | 
				
			||||||
@ -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"
 | 
					 | 
				
			||||||
@ -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."
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
@ -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",),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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"
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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
 | 
					 | 
				
			||||||
@ -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"
 | 
					 | 
				
			||||||
@ -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()
 | 
					 | 
				
			||||||
@ -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),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
@ -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")
 | 
					 | 
				
			||||||
@ -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")
 | 
					 | 
				
			||||||
@ -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),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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
 | 
					 | 
				
			||||||
@ -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/"
 | 
					 | 
				
			||||||
@ -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")
 | 
					 | 
				
			||||||
@ -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
 | 
					 | 
				
			||||||
@ -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")
 | 
					 | 
				
			||||||
@ -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)]
 | 
					 | 
				
			||||||
@ -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"
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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)
 | 
					 | 
				
			||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
"""Flow API Views"""
 | 
					"""Flow API Views"""
 | 
				
			||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					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):
 | 
					class FlowSerializer(ModelSerializer):
 | 
				
			||||||
@ -11,7 +11,7 @@ class FlowSerializer(ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = Flow
 | 
					        model = Flow
 | 
				
			||||||
        fields = ["pk", "name", "slug", "designation", "factors", "policies"]
 | 
					        fields = ["pk", "name", "slug", "designation", "stages", "policies"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowViewSet(ModelViewSet):
 | 
					class FlowViewSet(ModelViewSet):
 | 
				
			||||||
@ -21,17 +21,42 @@ class FlowViewSet(ModelViewSet):
 | 
				
			|||||||
    serializer_class = FlowSerializer
 | 
					    serializer_class = FlowSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowFactorBindingSerializer(ModelSerializer):
 | 
					class FlowStageBindingSerializer(ModelSerializer):
 | 
				
			||||||
    """FlowFactorBinding Serializer"""
 | 
					    """FlowStageBinding Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = FlowFactorBinding
 | 
					        model = FlowStageBinding
 | 
				
			||||||
        fields = ["pk", "flow", "factor", "re_evaluate_policies", "order", "policies"]
 | 
					        fields = ["pk", "flow", "stage", "re_evaluate_policies", "order", "policies"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowFactorBindingViewSet(ModelViewSet):
 | 
					class FlowStageBindingViewSet(ModelViewSet):
 | 
				
			||||||
    """FlowFactorBinding Viewset"""
 | 
					    """FlowStageBinding Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = FlowFactorBinding.objects.all()
 | 
					    queryset = FlowStageBinding.objects.all()
 | 
				
			||||||
    serializer_class = FlowFactorBindingSerializer
 | 
					    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()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,10 @@
 | 
				
			|||||||
"""factor forms"""
 | 
					"""Flow and Stage forms"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
					from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.flows.models import Flow, FlowFactorBinding
 | 
					from passbook.flows.models import Flow, FlowStageBinding
 | 
				
			||||||
 | 
					 | 
				
			||||||
GENERAL_FIELDS = ["name", "slug", "order", "policies", "enabled"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowForm(forms.ModelForm):
 | 
					class FlowForm(forms.ModelForm):
 | 
				
			||||||
@ -19,29 +17,30 @@ class FlowForm(forms.ModelForm):
 | 
				
			|||||||
            "name",
 | 
					            "name",
 | 
				
			||||||
            "slug",
 | 
					            "slug",
 | 
				
			||||||
            "designation",
 | 
					            "designation",
 | 
				
			||||||
            "factors",
 | 
					            "stages",
 | 
				
			||||||
            "policies",
 | 
					            "policies",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
            "factors": FilteredSelectMultiple(_("policies"), False),
 | 
					            "stages": FilteredSelectMultiple(_("stages"), False),
 | 
				
			||||||
 | 
					            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowFactorBindingForm(forms.ModelForm):
 | 
					class FlowStageBindingForm(forms.ModelForm):
 | 
				
			||||||
    """FlowFactorBinding Form"""
 | 
					    """FlowStageBinding Form"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = FlowFactorBinding
 | 
					        model = FlowStageBinding
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "flow",
 | 
					            "flow",
 | 
				
			||||||
            "factor",
 | 
					            "stage",
 | 
				
			||||||
            "re_evaluate_policies",
 | 
					            "re_evaluate_policies",
 | 
				
			||||||
            "order",
 | 
					            "order",
 | 
				
			||||||
            "policies",
 | 
					            "policies",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
            "factors": FilteredSelectMultiple(_("policies"), False),
 | 
					            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,8 +11,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_policies", "0001_initial"),
 | 
					        ("passbook_policies", "0003_auto_20200508_1642"),
 | 
				
			||||||
        ("passbook_core", "0011_auto_20200222_1822"),
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -37,6 +36,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                            ("AUTHENTICATION", "authentication"),
 | 
					                            ("AUTHENTICATION", "authentication"),
 | 
				
			||||||
                            ("ENROLLMENT", "enrollment"),
 | 
					                            ("ENROLLMENT", "enrollment"),
 | 
				
			||||||
                            ("RECOVERY", "recovery"),
 | 
					                            ("RECOVERY", "recovery"),
 | 
				
			||||||
 | 
					                            ("PASSWORD_CHANGE", "password_change"),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                        max_length=100,
 | 
					                        max_length=100,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
@ -55,7 +55,23 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            bases=("passbook_policies.policybindingmodel", models.Model),
 | 
					            bases=("passbook_policies.policybindingmodel", models.Model),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        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=[
 | 
					            fields=[
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "policybindingmodel_ptr",
 | 
					                    "policybindingmodel_ptr",
 | 
				
			||||||
@ -75,14 +91,14 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                        serialize=False,
 | 
					                        serialize=False,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                ("order", models.IntegerField()),
 | 
					 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "factor",
 | 
					                    "re_evaluate_policies",
 | 
				
			||||||
                    models.ForeignKey(
 | 
					                    models.BooleanField(
 | 
				
			||||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
					                        default=False,
 | 
				
			||||||
                        to="passbook_core.Factor",
 | 
					                        help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					                ("order", models.IntegerField()),
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "flow",
 | 
					                    "flow",
 | 
				
			||||||
                    models.ForeignKey(
 | 
					                    models.ForeignKey(
 | 
				
			||||||
@ -90,19 +106,29 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                        to="passbook_flows.Flow",
 | 
					                        to="passbook_flows.Flow",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "stage",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        to="passbook_flows.Stage",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                "verbose_name": "Flow Factor Binding",
 | 
					                "verbose_name": "Flow Stage Binding",
 | 
				
			||||||
                "verbose_name_plural": "Flow Factor Bindings",
 | 
					                "verbose_name_plural": "Flow Stage Bindings",
 | 
				
			||||||
                "unique_together": {("flow", "factor", "order")},
 | 
					                "ordering": ["order", "flow"],
 | 
				
			||||||
 | 
					                "unique_together": {("flow", "stage", "order")},
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bases=("passbook_policies.policybindingmodel", models.Model),
 | 
					            bases=("passbook_policies.policybindingmodel", models.Model),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name="flow",
 | 
					            model_name="flow",
 | 
				
			||||||
            name="factors",
 | 
					            name="stages",
 | 
				
			||||||
            field=models.ManyToManyField(
 | 
					            field=models.ManyToManyField(
 | 
				
			||||||
                through="passbook_flows.FlowFactorBinding", to="passbook_core.Factor"
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                through="passbook_flows.FlowStageBinding",
 | 
				
			||||||
 | 
					                to="passbook_flows.Stage",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -9,29 +9,35 @@ from passbook.flows.models import FlowDesignation
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
					def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
    Flow = apps.get_model("passbook_flows", "Flow")
 | 
					    Flow = apps.get_model("passbook_flows", "Flow")
 | 
				
			||||||
    FlowFactorBinding = apps.get_model("passbook_flows", "FlowFactorBinding")
 | 
					    FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
 | 
				
			||||||
    PasswordFactor = apps.get_model("passbook_factors_password", "PasswordFactor")
 | 
					    PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
 | 
				
			||||||
    db_alias = schema_editor.connection.alias
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if Flow.objects.using(db_alias).all().exists():
 | 
					    if Flow.objects.using(db_alias).all().exists():
 | 
				
			||||||
        # Only create default flow when none exist
 | 
					        # Only create default flow when none exist
 | 
				
			||||||
        return
 | 
					        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(
 | 
					    flow = Flow.objects.using(db_alias).create(
 | 
				
			||||||
        name="default-authentication-flow",
 | 
					        name="default-authentication-flow",
 | 
				
			||||||
        slug="default-authentication-flow",
 | 
					        slug="default-authentication-flow",
 | 
				
			||||||
        designation=FlowDesignation.AUTHENTICATION,
 | 
					        designation=FlowDesignation.AUTHENTICATION,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    FlowFactorBinding.objects.using(db_alias).create(
 | 
					    FlowStageBinding.objects.using(db_alias).create(
 | 
				
			||||||
        flow=flow, factor=pw_factor, order=0,
 | 
					        flow=flow, stage=pw_stage, order=0,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_flows", "0003_auto_20200508_1230"),
 | 
					        ("passbook_flows", "0001_initial"),
 | 
				
			||||||
 | 
					        ("passbook_stages_password", "0001_initial"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [migrations.RunPython(create_default_flow)]
 | 
					    operations = [migrations.RunPython(create_default_flow)]
 | 
				
			||||||
@ -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.",
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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",
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -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",
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,11 +1,12 @@
 | 
				
			|||||||
"""Flow models"""
 | 
					"""Flow models"""
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
from typing import Tuple
 | 
					from typing import Optional, Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					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.lib.models import UUIDModel
 | 
				
			||||||
from passbook.policies.models import PolicyBindingModel
 | 
					from passbook.policies.models import PolicyBindingModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,6 +18,7 @@ class FlowDesignation(Enum):
 | 
				
			|||||||
    AUTHENTICATION = "authentication"
 | 
					    AUTHENTICATION = "authentication"
 | 
				
			||||||
    ENROLLMENT = "enrollment"
 | 
					    ENROLLMENT = "enrollment"
 | 
				
			||||||
    RECOVERY = "recovery"
 | 
					    RECOVERY = "recovery"
 | 
				
			||||||
 | 
					    PASSWORD_CHANGE = "password_change"  # nosec # noqa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def as_choices() -> Tuple[Tuple[str, str]]:
 | 
					    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):
 | 
					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
 | 
					    a user. Additionally, policies can be applied, to specify which users
 | 
				
			||||||
    have access to this flow."""
 | 
					    have access to this flow."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,7 +58,7 @@ class Flow(PolicyBindingModel, UUIDModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    designation = models.CharField(max_length=100, choices=FlowDesignation.as_choices())
 | 
					    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(
 | 
					    pbm = models.OneToOneField(
 | 
				
			||||||
        PolicyBindingModel, parent_link=True, on_delete=models.CASCADE, related_name="+"
 | 
					        PolicyBindingModel, parent_link=True, on_delete=models.CASCADE, related_name="+"
 | 
				
			||||||
@ -51,13 +73,13 @@ class Flow(PolicyBindingModel, UUIDModel):
 | 
				
			|||||||
        verbose_name_plural = _("Flows")
 | 
					        verbose_name_plural = _("Flows")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowFactorBinding(PolicyBindingModel, UUIDModel):
 | 
					class FlowStageBinding(PolicyBindingModel, UUIDModel):
 | 
				
			||||||
    """Relationship between Flow and Factor. Order is required and unique for
 | 
					    """Relationship between Flow and Stage. Order is required and unique for
 | 
				
			||||||
    each flow-factor Binding. Additionally, policies can be specified, which determine if
 | 
					    each flow-stage Binding. Additionally, policies can be specified, which determine if
 | 
				
			||||||
    this Binding applies to the current user"""
 | 
					    this Binding applies to the current user"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    flow = models.ForeignKey("Flow", on_delete=models.CASCADE)
 | 
					    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(
 | 
					    re_evaluate_policies = models.BooleanField(
 | 
				
			||||||
        default=False,
 | 
					        default=False,
 | 
				
			||||||
@ -69,12 +91,12 @@ class FlowFactorBinding(PolicyBindingModel, UUIDModel):
 | 
				
			|||||||
    order = models.IntegerField()
 | 
					    order = models.IntegerField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    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:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ordering = ["order", "flow"]
 | 
					        ordering = ["order", "flow"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Flow Factor Binding")
 | 
					        verbose_name = _("Flow Stage Binding")
 | 
				
			||||||
        verbose_name_plural = _("Flow Factor Bindings")
 | 
					        verbose_name_plural = _("Flow Stage Bindings")
 | 
				
			||||||
        unique_together = (("flow", "factor", "order"),)
 | 
					        unique_together = (("flow", "stage", "order"),)
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.http import HttpRequest
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.flows.exceptions import FlowNonApplicableError
 | 
					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
 | 
					from passbook.policies.engine import PolicyEngine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
@ -19,19 +19,19 @@ PLAN_CONTEXT_SSO = "is_sso"
 | 
				
			|||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class FlowPlan:
 | 
					class FlowPlan:
 | 
				
			||||||
    """This data-class is the output of a FlowPlanner. It holds a flat list
 | 
					    """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)
 | 
					    context: Dict[str, Any] = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def next(self) -> Factor:
 | 
					    def next(self) -> Stage:
 | 
				
			||||||
        """Return next pending factor from the bottom of the list"""
 | 
					        """Return next pending stage from the bottom of the list"""
 | 
				
			||||||
        factor_cls = self.factors.pop(0)
 | 
					        stage_cls = self.stages.pop(0)
 | 
				
			||||||
        return factor_cls
 | 
					        return stage_cls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FlowPlanner:
 | 
					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."""
 | 
					    that should be applied."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    flow: Flow
 | 
					    flow: Flow
 | 
				
			||||||
@ -45,7 +45,7 @@ class FlowPlanner:
 | 
				
			|||||||
        return engine.result
 | 
					        return engine.result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def plan(self, request: HttpRequest) -> FlowPlan:
 | 
					    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"""
 | 
					        and return ordered list"""
 | 
				
			||||||
        LOGGER.debug("Starting planning process", flow=self.flow)
 | 
					        LOGGER.debug("Starting planning process", flow=self.flow)
 | 
				
			||||||
        start_time = time()
 | 
					        start_time = time()
 | 
				
			||||||
@ -56,13 +56,18 @@ class FlowPlanner:
 | 
				
			|||||||
        if not root_passing:
 | 
					        if not root_passing:
 | 
				
			||||||
            raise FlowNonApplicableError(root_passing_messages)
 | 
					            raise FlowNonApplicableError(root_passing_messages)
 | 
				
			||||||
        # Check Flow policies
 | 
					        # Check Flow policies
 | 
				
			||||||
        for factor in self.flow.factors.order_by("order").select_subclasses():
 | 
					        for stage in (
 | 
				
			||||||
            engine = PolicyEngine(factor.policies.all(), request.user, request)
 | 
					            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()
 | 
					            engine.build()
 | 
				
			||||||
            passing, _ = engine.result
 | 
					            passing, _ = engine.result
 | 
				
			||||||
            if passing:
 | 
					            if passing:
 | 
				
			||||||
                LOGGER.debug("Factor passing", factor=factor)
 | 
					                LOGGER.debug("Stage passing", stage=stage)
 | 
				
			||||||
                plan.factors.append(factor)
 | 
					                plan.stages.append(stage)
 | 
				
			||||||
        end_time = time()
 | 
					        end_time = time()
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Finished planning", flow=self.flow, duration_s=end_time - start_time
 | 
					            "Finished planning", flow=self.flow, duration_s=end_time - start_time
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""passbook multi-factor authentication engine"""
 | 
					"""passbook stage Base view"""
 | 
				
			||||||
from typing import Any, Dict
 | 
					from typing import Any, Dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.forms import ModelForm
 | 
					from django.forms import ModelForm
 | 
				
			||||||
@ -11,8 +11,8 @@ from passbook.flows.views import FlowExecutorView
 | 
				
			|||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticationFactor(TemplateView):
 | 
					class AuthenticationStage(TemplateView):
 | 
				
			||||||
    """Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
 | 
					    """Abstract Authentication stage, inherits TemplateView but can be combined with FormView"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form: ModelForm = None
 | 
					    form: ModelForm = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""passbook multi-factor authentication engine"""
 | 
					"""passbook multi-stage authentication engine"""
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth import login
 | 
					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 django.views.generic import View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.models import Factor
 | 
					 | 
				
			||||||
from passbook.core.views.utils import PermissionDeniedView
 | 
					from passbook.core.views.utils import PermissionDeniedView
 | 
				
			||||||
from passbook.flows.exceptions import FlowNonApplicableError
 | 
					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.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
 | 
				
			||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
 | 
					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):
 | 
					class FlowExecutorView(View):
 | 
				
			||||||
    """Stage 1 Flow executor, passing requests to Factor Views"""
 | 
					    """Stage 1 Flow executor, passing requests to Stage Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    flow: Flow
 | 
					    flow: Flow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    plan: FlowPlan
 | 
					    plan: FlowPlan
 | 
				
			||||||
    current_factor: Factor
 | 
					    current_stage: Stage
 | 
				
			||||||
    current_factor_view: View
 | 
					    current_stage_view: View
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setup(self, request: HttpRequest, flow_slug: str):
 | 
					    def setup(self, request: HttpRequest, flow_slug: str):
 | 
				
			||||||
        super().setup(request, flow_slug=flow_slug)
 | 
					        super().setup(request, flow_slug=flow_slug)
 | 
				
			||||||
@ -77,36 +76,34 @@ class FlowExecutorView(View):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            LOGGER.debug("Continuing existing plan", flow_slug=flow_slug)
 | 
					            LOGGER.debug("Continuing existing plan", flow_slug=flow_slug)
 | 
				
			||||||
            self.plan = self.request.session[SESSION_KEY_PLAN]
 | 
					            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
 | 
					        # as it hasn't been successfully passed yet
 | 
				
			||||||
        self.current_factor = self.plan.next()
 | 
					        self.current_stage = self.plan.next()
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Current factor",
 | 
					            "Current stage", current_stage=self.current_stage, flow_slug=self.flow.slug,
 | 
				
			||||||
            current_factor=self.current_factor,
 | 
					 | 
				
			||||||
            flow_slug=self.flow.slug,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        factor_cls = path_to_class(self.current_factor.type)
 | 
					        stage_cls = path_to_class(self.current_stage.type)
 | 
				
			||||||
        self.current_factor_view = factor_cls(self)
 | 
					        self.current_stage_view = stage_cls(self)
 | 
				
			||||||
        self.current_factor_view.request = request
 | 
					        self.current_stage_view.request = request
 | 
				
			||||||
        return super().dispatch(request)
 | 
					        return super().dispatch(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        """pass get request to current factor"""
 | 
					        """pass get request to current stage"""
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Passing GET",
 | 
					            "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,
 | 
					            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:
 | 
					    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        """pass post request to current factor"""
 | 
					        """pass post request to current stage"""
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Passing POST",
 | 
					            "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,
 | 
					            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:
 | 
					    def _initiate_plan(self) -> FlowPlan:
 | 
				
			||||||
        planner = FlowPlanner(self.flow)
 | 
					        planner = FlowPlanner(self.flow)
 | 
				
			||||||
@ -115,7 +112,7 @@ class FlowExecutorView(View):
 | 
				
			|||||||
        return plan
 | 
					        return plan
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _flow_done(self) -> HttpResponse:
 | 
					    def _flow_done(self) -> HttpResponse:
 | 
				
			||||||
        """User Successfully passed all factors"""
 | 
					        """User Successfully passed all stages"""
 | 
				
			||||||
        backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend
 | 
					        backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend
 | 
				
			||||||
        login(
 | 
					        login(
 | 
				
			||||||
            self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend
 | 
					            self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend
 | 
				
			||||||
@ -131,34 +128,34 @@ class FlowExecutorView(View):
 | 
				
			|||||||
            return redirect(next_param)
 | 
					            return redirect(next_param)
 | 
				
			||||||
        return redirect_with_qs("passbook_core:overview")
 | 
					        return redirect_with_qs("passbook_core:overview")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def factor_ok(self) -> HttpResponse:
 | 
					    def stage_ok(self) -> HttpResponse:
 | 
				
			||||||
        """Callback called by factors upon successful completion.
 | 
					        """Callback called by stages upon successful completion.
 | 
				
			||||||
        Persists updated plan and context to session."""
 | 
					        Persists updated plan and context to session."""
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Factor ok",
 | 
					            "Stage ok",
 | 
				
			||||||
            factor_class=class_to_path(self.current_factor_view.__class__),
 | 
					            stage_class=class_to_path(self.current_stage_view.__class__),
 | 
				
			||||||
            flow_slug=self.flow.slug,
 | 
					            flow_slug=self.flow.slug,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.request.session[SESSION_KEY_PLAN] = self.plan
 | 
					        self.request.session[SESSION_KEY_PLAN] = self.plan
 | 
				
			||||||
        if self.plan.factors:
 | 
					        if self.plan.stages:
 | 
				
			||||||
            LOGGER.debug(
 | 
					            LOGGER.debug(
 | 
				
			||||||
                "Continuing with next factor",
 | 
					                "Continuing with next stage",
 | 
				
			||||||
                reamining=len(self.plan.factors),
 | 
					                reamining=len(self.plan.stages),
 | 
				
			||||||
                flow_slug=self.flow.slug,
 | 
					                flow_slug=self.flow.slug,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return redirect_with_qs(
 | 
					            return redirect_with_qs(
 | 
				
			||||||
                "passbook_flows:flow-executor", self.request.GET, **self.kwargs
 | 
					                "passbook_flows:flow-executor", self.request.GET, **self.kwargs
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        # User passed all factors
 | 
					        # User passed all stages
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "User passed all factors",
 | 
					            "User passed all stages",
 | 
				
			||||||
            user=self.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
					            user=self.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
				
			||||||
            flow_slug=self.flow.slug,
 | 
					            flow_slug=self.flow.slug,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return self._flow_done()
 | 
					        return self._flow_done()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def factor_invalid(self) -> HttpResponse:
 | 
					    def stage_invalid(self) -> HttpResponse:
 | 
				
			||||||
        """Callback used factor when data is correct but a policy denies access
 | 
					        """Callback used stage when data is correct but a policy denies access
 | 
				
			||||||
        or the user account is disabled."""
 | 
					        or the user account is disabled."""
 | 
				
			||||||
        LOGGER.debug("User invalid", flow_slug=self.flow.slug)
 | 
					        LOGGER.debug("User invalid", flow_slug=self.flow.slug)
 | 
				
			||||||
        self.cancel()
 | 
					        self.cancel()
 | 
				
			||||||
 | 
				
			|||||||
@ -96,11 +96,11 @@ INSTALLED_APPS = [
 | 
				
			|||||||
    "passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
 | 
					    "passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
 | 
				
			||||||
    "passbook.providers.saml.apps.PassbookProviderSAMLConfig",
 | 
					    "passbook.providers.saml.apps.PassbookProviderSAMLConfig",
 | 
				
			||||||
    "passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
 | 
					    "passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
 | 
				
			||||||
    "passbook.factors.otp.apps.PassbookFactorOTPConfig",
 | 
					    "passbook.stages.otp.apps.PassbookStageOTPConfig",
 | 
				
			||||||
    "passbook.factors.captcha.apps.PassbookFactorCaptchaConfig",
 | 
					    "passbook.stages.captcha.apps.PassbookStageCaptchaConfig",
 | 
				
			||||||
    "passbook.factors.password.apps.PassbookFactorPasswordConfig",
 | 
					    "passbook.stages.password.apps.PassbookStagePasswordConfig",
 | 
				
			||||||
    "passbook.factors.dummy.apps.PassbookFactorDummyConfig",
 | 
					    "passbook.stages.dummy.apps.PassbookStageDummyConfig",
 | 
				
			||||||
    "passbook.factors.email.apps.PassbookFactorEmailConfig",
 | 
					    "passbook.stages.email.apps.PassbookStageEmailConfig",
 | 
				
			||||||
    "passbook.policies.expiry.apps.PassbookPolicyExpiryConfig",
 | 
					    "passbook.policies.expiry.apps.PassbookPolicyExpiryConfig",
 | 
				
			||||||
    "passbook.policies.reputation.apps.PassbookPolicyReputationConfig",
 | 
					    "passbook.policies.reputation.apps.PassbookPolicyReputationConfig",
 | 
				
			||||||
    "passbook.policies.hibp.apps.PassbookPolicyHIBPConfig",
 | 
					    "passbook.policies.hibp.apps.PassbookPolicyHIBPConfig",
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,6 @@ from django.views.generic import RedirectView, View
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.audit.models import Event, EventAction
 | 
					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.models import Flow, FlowDesignation
 | 
				
			||||||
from passbook.flows.planner import (
 | 
					from passbook.flows.planner import (
 | 
				
			||||||
    PLAN_CONTEXT_PENDING_USER,
 | 
					    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.lib.utils.urls import redirect_with_qs
 | 
				
			||||||
from passbook.sources.oauth.clients import get_client
 | 
					from passbook.sources.oauth.clients import get_client
 | 
				
			||||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
 | 
					from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
 | 
				
			||||||
 | 
					from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -169,7 +169,7 @@ class OAuthCallback(OAuthClientMixin, View):
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_login(self, user, source, access):
 | 
					    def handle_login(self, user, source, access):
 | 
				
			||||||
        """Prepare AuthenticationView, redirect users to remaining Factors"""
 | 
					        """Prepare Authentication Plan, redirect user FlowExecutor"""
 | 
				
			||||||
        user = authenticate(
 | 
					        user = authenticate(
 | 
				
			||||||
            source=access.source, identifier=access.identifier, request=self.request
 | 
					            source=access.source, identifier=access.identifier, request=self.request
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								passbook/stages/captcha/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								passbook/stages/captcha/api.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
							
								
								
									
										10
									
								
								passbook/stages/captcha/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/stages/captcha/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
				
			||||||
							
								
								
									
										25
									
								
								passbook/stages/captcha/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								passbook/stages/captcha/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -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(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
							
								
								
									
										49
									
								
								passbook/stages/captcha/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								passbook/stages/captcha/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -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",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -1,12 +1,12 @@
 | 
				
			|||||||
"""passbook captcha factor"""
 | 
					"""passbook captcha stage"""
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.models import Factor
 | 
					from passbook.flows.models import Stage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CaptchaFactor(Factor):
 | 
					class CaptchaStage(Stage):
 | 
				
			||||||
    """Captcha Factor instance"""
 | 
					    """Captcha Stage instance"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public_key = models.TextField(
 | 
					    public_key = models.TextField(
 | 
				
			||||||
        help_text=_(
 | 
					        help_text=_(
 | 
				
			||||||
@ -19,13 +19,13 @@ class CaptchaFactor(Factor):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    type = "passbook.factors.captcha.factor.CaptchaFactor"
 | 
					    type = "passbook.stages.captcha.stage.CaptchaStage"
 | 
				
			||||||
    form = "passbook.factors.captcha.forms.CaptchaFactorForm"
 | 
					    form = "passbook.stages.captcha.forms.CaptchaStageForm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Captcha Factor {self.slug}"
 | 
					        return f"Captcha Stage {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Captcha Factor")
 | 
					        verbose_name = _("Captcha Stage")
 | 
				
			||||||
        verbose_name_plural = _("Captcha Factors")
 | 
					        verbose_name_plural = _("Captcha Stages")
 | 
				
			||||||
@ -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
 | 
					# 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_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
 | 
				
			||||||
RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
 | 
					RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
 | 
				
			||||||
@ -1,23 +1,23 @@
 | 
				
			|||||||
"""passbook captcha factor"""
 | 
					"""passbook captcha stage"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.views.generic import FormView
 | 
					from django.views.generic import FormView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.captcha.forms import CaptchaForm
 | 
					from passbook.flows.stage import AuthenticationStage
 | 
				
			||||||
from passbook.flows.factor_base import AuthenticationFactor
 | 
					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"""
 | 
					    """Simple captcha checker, logic is handeled in django-captcha module"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = CaptchaForm
 | 
					    form_class = CaptchaForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        return self.executor.factor_ok()
 | 
					        return self.executor.stage_ok()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_form(self, form_class=None):
 | 
					    def get_form(self, form_class=None):
 | 
				
			||||||
        form = CaptchaForm(**self.get_form_kwargs())
 | 
					        form = CaptchaForm(**self.get_form_kwargs())
 | 
				
			||||||
        form.fields["captcha"].public_key = self.executor.current_factor.public_key
 | 
					        form.fields["captcha"].public_key = self.executor.current_stage.public_key
 | 
				
			||||||
        form.fields["captcha"].private_key = self.executor.current_factor.private_key
 | 
					        form.fields["captcha"].private_key = self.executor.current_stage.private_key
 | 
				
			||||||
        form.fields["captcha"].widget.attrs["data-sitekey"] = form.fields[
 | 
					        form.fields["captcha"].widget.attrs["data-sitekey"] = form.fields[
 | 
				
			||||||
            "captcha"
 | 
					            "captcha"
 | 
				
			||||||
        ].public_key
 | 
					        ].public_key
 | 
				
			||||||
							
								
								
									
										21
									
								
								passbook/stages/dummy/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								passbook/stages/dummy/api.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
							
								
								
									
										11
									
								
								passbook/stages/dummy/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/stages/dummy/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
				
			||||||
							
								
								
									
										16
									
								
								passbook/stages/dummy/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/stages/dummy/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -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(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
@ -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
 | 
					import django.db.models.deletion
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
@ -9,29 +9,29 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_core", "0001_initial"),
 | 
					        ("passbook_flows", "0001_initial"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name="DummyFactor",
 | 
					            name="DummyStage",
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "factor_ptr",
 | 
					                    "stage_ptr",
 | 
				
			||||||
                    models.OneToOneField(
 | 
					                    models.OneToOneField(
 | 
				
			||||||
                        auto_created=True,
 | 
					                        auto_created=True,
 | 
				
			||||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
                        parent_link=True,
 | 
					                        parent_link=True,
 | 
				
			||||||
                        primary_key=True,
 | 
					                        primary_key=True,
 | 
				
			||||||
                        serialize=False,
 | 
					                        serialize=False,
 | 
				
			||||||
                        to="passbook_core.Factor",
 | 
					                        to="passbook_flows.Stage",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                "verbose_name": "Dummy Factor",
 | 
					                "verbose_name": "Dummy Stage",
 | 
				
			||||||
                "verbose_name_plural": "Dummy Factors",
 | 
					                "verbose_name_plural": "Dummy Stages",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bases=("passbook_core.factor",),
 | 
					            bases=("passbook_flows.stage",),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										19
									
								
								passbook/stages/dummy/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								passbook/stages/dummy/models.py
									
									
									
									
									
										Normal file
									
								
							@ -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")
 | 
				
			||||||
							
								
								
									
										12
									
								
								passbook/stages/dummy/stage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								passbook/stages/dummy/stage.py
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
				
			||||||
@ -1,22 +1,19 @@
 | 
				
			|||||||
"""EmailFactor API Views"""
 | 
					"""EmailStage API Views"""
 | 
				
			||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.email.models import EmailFactor
 | 
					from passbook.stages.email.models import EmailStage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailFactorSerializer(ModelSerializer):
 | 
					class EmailStageSerializer(ModelSerializer):
 | 
				
			||||||
    """EmailFactor Serializer"""
 | 
					    """EmailStage Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = EmailFactor
 | 
					        model = EmailStage
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "pk",
 | 
					            "pk",
 | 
				
			||||||
            "name",
 | 
					            "name",
 | 
				
			||||||
            "slug",
 | 
					 | 
				
			||||||
            "order",
 | 
					 | 
				
			||||||
            "enabled",
 | 
					 | 
				
			||||||
            "host",
 | 
					            "host",
 | 
				
			||||||
            "port",
 | 
					            "port",
 | 
				
			||||||
            "username",
 | 
					            "username",
 | 
				
			||||||
@ -31,8 +28,8 @@ class EmailFactorSerializer(ModelSerializer):
 | 
				
			|||||||
        extra_kwargs = {"password": {"write_only": True}}
 | 
					        extra_kwargs = {"password": {"write_only": True}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailFactorViewSet(ModelViewSet):
 | 
					class EmailStageViewSet(ModelViewSet):
 | 
				
			||||||
    """EmailFactor Viewset"""
 | 
					    """EmailStage Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = EmailFactor.objects.all()
 | 
					    queryset = EmailStage.objects.all()
 | 
				
			||||||
    serializer_class = EmailFactorSerializer
 | 
					    serializer_class = EmailStageSerializer
 | 
				
			||||||
							
								
								
									
										15
									
								
								passbook/stages/email/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								passbook/stages/email/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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")
 | 
				
			||||||
@ -1,19 +1,18 @@
 | 
				
			|||||||
"""passbook administration forms"""
 | 
					"""passbook administration forms"""
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
					 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.email.models import EmailFactor
 | 
					from passbook.stages.email.models import EmailStage
 | 
				
			||||||
from passbook.flows.forms import GENERAL_FIELDS
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailFactorForm(forms.ModelForm):
 | 
					class EmailStageForm(forms.ModelForm):
 | 
				
			||||||
    """Form to create/edit Dummy Factor"""
 | 
					    """Form to create/edit Dummy Stage"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = EmailFactor
 | 
					        model = EmailStage
 | 
				
			||||||
        fields = GENERAL_FIELDS + [
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "name",
 | 
				
			||||||
            "host",
 | 
					            "host",
 | 
				
			||||||
            "port",
 | 
					            "port",
 | 
				
			||||||
            "username",
 | 
					            "username",
 | 
				
			||||||
@ -27,8 +26,6 @@ class EmailFactorForm(forms.ModelForm):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
            "order": forms.NumberInput(),
 | 
					 | 
				
			||||||
            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
					 | 
				
			||||||
            "host": forms.TextInput(),
 | 
					            "host": forms.TextInput(),
 | 
				
			||||||
            "username": forms.TextInput(),
 | 
					            "username": forms.TextInput(),
 | 
				
			||||||
            "password": forms.TextInput(),
 | 
					            "password": forms.TextInput(),
 | 
				
			||||||
@ -41,8 +38,3 @@ class EmailFactorForm(forms.ModelForm):
 | 
				
			|||||||
            "ssl_keyfile": _("SSL Keyfile (optional)"),
 | 
					            "ssl_keyfile": _("SSL Keyfile (optional)"),
 | 
				
			||||||
            "ssl_certfile": _("SSL Certfile (optional)"),
 | 
					            "ssl_certfile": _("SSL Certfile (optional)"),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        help_texts = {
 | 
					 | 
				
			||||||
            "policies": _(
 | 
					 | 
				
			||||||
                "Policies which determine if this factor applies to the current user."
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
@ -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
 | 
					import django.db.models.deletion
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
@ -9,22 +9,22 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_core", "0001_initial"),
 | 
					        ("passbook_flows", "0001_initial"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name="EmailFactor",
 | 
					            name="EmailStage",
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "factor_ptr",
 | 
					                    "stage_ptr",
 | 
				
			||||||
                    models.OneToOneField(
 | 
					                    models.OneToOneField(
 | 
				
			||||||
                        auto_created=True,
 | 
					                        auto_created=True,
 | 
				
			||||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
                        parent_link=True,
 | 
					                        parent_link=True,
 | 
				
			||||||
                        primary_key=True,
 | 
					                        primary_key=True,
 | 
				
			||||||
                        serialize=False,
 | 
					                        serialize=False,
 | 
				
			||||||
                        to="passbook_core.Factor",
 | 
					                        to="passbook_flows.Stage",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                ("host", models.TextField(default="localhost")),
 | 
					                ("host", models.TextField(default="localhost")),
 | 
				
			||||||
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                ("password", models.TextField(blank=True, default="")),
 | 
					                ("password", models.TextField(blank=True, default="")),
 | 
				
			||||||
                ("use_tls", models.BooleanField(default=False)),
 | 
					                ("use_tls", models.BooleanField(default=False)),
 | 
				
			||||||
                ("use_ssl", 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_keyfile", models.TextField(blank=True, default=None, null=True)),
 | 
				
			||||||
                ("ssl_certfile", 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={
 | 
					            options={
 | 
				
			||||||
                "verbose_name": "Email Factor",
 | 
					                "verbose_name": "Email Stage",
 | 
				
			||||||
                "verbose_name_plural": "Email Factors",
 | 
					                "verbose_name_plural": "Email Stages",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bases=("passbook_core.factor",),
 | 
					            bases=("passbook_flows.stage",),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
@ -1,13 +1,13 @@
 | 
				
			|||||||
"""email factor models"""
 | 
					"""email stage models"""
 | 
				
			||||||
from django.core.mail.backends.smtp import EmailBackend
 | 
					from django.core.mail.backends.smtp import EmailBackend
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.models import Factor
 | 
					from passbook.flows.models import Stage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailFactor(Factor):
 | 
					class EmailStage(Stage):
 | 
				
			||||||
    """email factor"""
 | 
					    """email stage"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    host = models.TextField(default="localhost")
 | 
					    host = models.TextField(default="localhost")
 | 
				
			||||||
    port = models.IntegerField(default=25)
 | 
					    port = models.IntegerField(default=25)
 | 
				
			||||||
@ -22,8 +22,8 @@ class EmailFactor(Factor):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    from_address = models.EmailField(default="system@passbook.local")
 | 
					    from_address = models.EmailField(default="system@passbook.local")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    type = "passbook.factors.email.factor.EmailFactorView"
 | 
					    type = "passbook.stages.email.stage.EmailStageView"
 | 
				
			||||||
    form = "passbook.factors.email.forms.EmailFactorForm"
 | 
					    form = "passbook.stages.email.forms.EmailStageForm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def backend(self) -> EmailBackend:
 | 
					    def backend(self) -> EmailBackend:
 | 
				
			||||||
@ -41,9 +41,9 @@ class EmailFactor(Factor):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Email Factor {self.slug}"
 | 
					        return f"Email Stage {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Email Factor")
 | 
					        verbose_name = _("Email Stage")
 | 
				
			||||||
        verbose_name_plural = _("Email Factors")
 | 
					        verbose_name_plural = _("Email Stages")
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""passbook multi-factor authentication engine"""
 | 
					"""passbook multi-stage authentication engine"""
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.shortcuts import reverse
 | 
					from django.shortcuts import reverse
 | 
				
			||||||
@ -6,17 +6,17 @@ from django.utils.translation import gettext as _
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.models import Nonce
 | 
					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.planner import PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
 | 
					from passbook.flows.stage import AuthenticationStage
 | 
				
			||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
 | 
					from passbook.stages.email.tasks import send_mails
 | 
				
			||||||
 | 
					from passbook.stages.email.utils import TemplateEmailMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailFactorView(AuthenticationFactor):
 | 
					class EmailStageView(AuthenticationStage):
 | 
				
			||||||
    """Dummy factor for testing with multiple factors"""
 | 
					    """E-Mail stage which sends E-Mail for verification"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs["show_password_forget_notice"] = CONFIG.y(
 | 
					        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."))
 | 
					        messages.success(request, _("Check your E-Mails for a password reset link."))
 | 
				
			||||||
        return self.executor.cancel()
 | 
					        return self.executor.cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request: HttpRequest):
 | 
					    def post(self, request: HttpRequest):
 | 
				
			||||||
        """Just redirect to next factor"""
 | 
					        """Just redirect to next stage"""
 | 
				
			||||||
        return self.executor.factor_ok()
 | 
					        return self.executor.stage_ok()
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""email factor tasks"""
 | 
					"""email stage tasks"""
 | 
				
			||||||
from smtplib import SMTPException
 | 
					from smtplib import SMTPException
 | 
				
			||||||
from typing import Any, Dict, List
 | 
					from typing import Any, Dict, List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -6,38 +6,38 @@ from celery import group
 | 
				
			|||||||
from django.core.mail import EmailMessage
 | 
					from django.core.mail import EmailMessage
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.email.models import EmailFactor
 | 
					 | 
				
			||||||
from passbook.root.celery import CELERY_APP
 | 
					from passbook.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					from passbook.stages.email.models import EmailStage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					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"""
 | 
					    """Wrapper to convert EmailMessage to dict and send it from worker"""
 | 
				
			||||||
    tasks = []
 | 
					    tasks = []
 | 
				
			||||||
    for message in messages:
 | 
					    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)
 | 
					    lazy_group = group(*tasks)
 | 
				
			||||||
    promise = lazy_group()
 | 
					    promise = lazy_group()
 | 
				
			||||||
    return promise
 | 
					    return promise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True)
 | 
					@CELERY_APP.task(bind=True)
 | 
				
			||||||
def _send_mail_task(self, email_factor_pk: int, message: Dict[Any, Any]):
 | 
					def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
 | 
				
			||||||
    """Send E-Mail according to EmailFactor parameters from background worker.
 | 
					    """Send E-Mail according to EmailStage parameters from background worker.
 | 
				
			||||||
    Automatically retries if message couldn't be sent."""
 | 
					    Automatically retries if message couldn't be sent."""
 | 
				
			||||||
    factor: EmailFactor = EmailFactor.objects.get(pk=email_factor_pk)
 | 
					    stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
 | 
				
			||||||
    backend = factor.backend
 | 
					    backend = stage.backend
 | 
				
			||||||
    backend.open()
 | 
					    backend.open()
 | 
				
			||||||
    # Since django's EmailMessage objects are not JSON serialisable,
 | 
					    # Since django's EmailMessage objects are not JSON serialisable,
 | 
				
			||||||
    # we need to rebuild them from a dict
 | 
					    # we need to rebuild them from a dict
 | 
				
			||||||
    message_object = EmailMessage()
 | 
					    message_object = EmailMessage()
 | 
				
			||||||
    for key, value in message.items():
 | 
					    for key, value in message.items():
 | 
				
			||||||
        setattr(message_object, key, value)
 | 
					        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)
 | 
					    LOGGER.debug("Sending mail", to=message_object.to)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        num_sent = factor.backend.send_messages([message_object])
 | 
					        num_sent = stage.backend.send_messages([message_object])
 | 
				
			||||||
    except SMTPException as exc:
 | 
					    except SMTPException as exc:
 | 
				
			||||||
        raise self.retry(exc=exc)
 | 
					        raise self.retry(exc=exc)
 | 
				
			||||||
    if num_sent != 1:
 | 
					    if num_sent != 1:
 | 
				
			||||||
							
								
								
									
										21
									
								
								passbook/stages/otp/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								passbook/stages/otp/api.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
							
								
								
									
										12
									
								
								passbook/stages/otp/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								passbook/stages/otp/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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/"
 | 
				
			||||||
@ -1,14 +1,12 @@
 | 
				
			|||||||
"""passbook OTP Forms"""
 | 
					"""passbook OTP Forms"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
					 | 
				
			||||||
from django.core.validators import RegexValidator
 | 
					from django.core.validators import RegexValidator
 | 
				
			||||||
from django.utils.safestring import mark_safe
 | 
					from django.utils.safestring import mark_safe
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from django_otp.models import Device
 | 
					from django_otp.models import Device
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.otp.models import OTPFactor
 | 
					from passbook.stages.otp.models import OTPStage
 | 
				
			||||||
from passbook.flows.forms import GENERAL_FIELDS
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
OTP_CODE_VALIDATOR = RegexValidator(
 | 
					OTP_CODE_VALIDATOR = RegexValidator(
 | 
				
			||||||
    r"^[0-9a-z]{6,8}$", _("Only alpha-numeric characters are allowed.")
 | 
					    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")
 | 
					        return self.cleaned_data.get("code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OTPFactorForm(forms.ModelForm):
 | 
					class OTPStageForm(forms.ModelForm):
 | 
				
			||||||
    """Form to edit OTPFactor instances"""
 | 
					    """Form to edit OTPStage instances"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = OTPFactor
 | 
					        model = OTPStage
 | 
				
			||||||
        fields = GENERAL_FIELDS + ["enforced"]
 | 
					        fields = ["name", "enforced"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
            "order": forms.NumberInput(),
 | 
					 | 
				
			||||||
            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        help_texts = {
 | 
					 | 
				
			||||||
            "policies": _(
 | 
					 | 
				
			||||||
                "Policies which determine if this factor applies to the current user."
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -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
 | 
					import django.db.models.deletion
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
@ -9,36 +9,33 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_core", "0001_initial"),
 | 
					        ("passbook_flows", "0001_initial"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name="OTPFactor",
 | 
					            name="OTPStage",
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "factor_ptr",
 | 
					                    "stage_ptr",
 | 
				
			||||||
                    models.OneToOneField(
 | 
					                    models.OneToOneField(
 | 
				
			||||||
                        auto_created=True,
 | 
					                        auto_created=True,
 | 
				
			||||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
                        parent_link=True,
 | 
					                        parent_link=True,
 | 
				
			||||||
                        primary_key=True,
 | 
					                        primary_key=True,
 | 
				
			||||||
                        serialize=False,
 | 
					                        serialize=False,
 | 
				
			||||||
                        to="passbook_core.Factor",
 | 
					                        to="passbook_flows.Stage",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "enforced",
 | 
					                    "enforced",
 | 
				
			||||||
                    models.BooleanField(
 | 
					                    models.BooleanField(
 | 
				
			||||||
                        default=False,
 | 
					                        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={
 | 
					            options={"verbose_name": "OTP Stage", "verbose_name_plural": "OTP Stages",},
 | 
				
			||||||
                "verbose_name": "OTP Factor",
 | 
					            bases=("passbook_flows.stage",),
 | 
				
			||||||
                "verbose_name_plural": "OTP Factors",
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            bases=("passbook_core.factor",),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										34
									
								
								passbook/stages/otp/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								passbook/stages/otp/models.py
									
									
									
									
									
										Normal file
									
								
							@ -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")
 | 
				
			||||||
@ -1,22 +1,22 @@
 | 
				
			|||||||
"""OTP Factor logic"""
 | 
					"""OTP Stage logic"""
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django.views.generic import FormView
 | 
					from django.views.generic import FormView
 | 
				
			||||||
from django_otp import match_token, user_has_device
 | 
					from django_otp import match_token, user_has_device
 | 
				
			||||||
from structlog import get_logger
 | 
					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.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()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OTPFactor(FormView, AuthenticationFactor):
 | 
					class OTPStage(FormView, AuthenticationStage):
 | 
				
			||||||
    """OTP Factor View"""
 | 
					    """OTP Stage View"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "otp/factor.html"
 | 
					    template_name = "stages/otp/stage.html"
 | 
				
			||||||
    form_class = OTPVerifyForm
 | 
					    form_class = OTPVerifyForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
@ -29,7 +29,7 @@ class OTPFactor(FormView, AuthenticationFactor):
 | 
				
			|||||||
        pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
 | 
					        pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
 | 
				
			||||||
        if not user_has_device(pending_user):
 | 
					        if not user_has_device(pending_user):
 | 
				
			||||||
            LOGGER.debug("User doesn't have OTP Setup.")
 | 
					            LOGGER.debug("User doesn't have OTP Setup.")
 | 
				
			||||||
            if self.executor.current_factor.enforced:
 | 
					            if self.executor.current_stage.enforced:
 | 
				
			||||||
                # Redirect to setup view
 | 
					                # Redirect to setup view
 | 
				
			||||||
                LOGGER.debug("OTP is enforced, redirecting to setup")
 | 
					                LOGGER.debug("OTP is enforced, redirecting to setup")
 | 
				
			||||||
                request.user = pending_user
 | 
					                request.user = pending_user
 | 
				
			||||||
@ -54,6 +54,6 @@ class OTPFactor(FormView, AuthenticationFactor):
 | 
				
			|||||||
            form.cleaned_data.get("code"),
 | 
					            form.cleaned_data.get("code"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if device:
 | 
					        if device:
 | 
				
			||||||
            return self.executor.factor_ok()
 | 
					            return self.executor.stage_ok()
 | 
				
			||||||
        messages.error(self.request, _("Invalid OTP."))
 | 
					        messages.error(self.request, _("Invalid OTP."))
 | 
				
			||||||
        return self.form_invalid(form)
 | 
					        return self.form_invalid(form)
 | 
				
			||||||
@ -23,10 +23,10 @@
 | 
				
			|||||||
                </p>
 | 
					                </p>
 | 
				
			||||||
                <p>
 | 
					                <p>
 | 
				
			||||||
                    {% if not state %}
 | 
					                    {% if not state %}
 | 
				
			||||||
                    <a href="{% url 'passbook_factors_otp:otp-enable' %}"
 | 
					                    <a href="{% url 'passbook_stages_otp:otp-enable' %}"
 | 
				
			||||||
                    class="btn btn-success btn-sm">{% trans "Enable OTP" %}</a>
 | 
					                    class="btn btn-success btn-sm">{% trans "Enable OTP" %}</a>
 | 
				
			||||||
                    {% else %}
 | 
					                    {% else %}
 | 
				
			||||||
                    <a href="{% url 'passbook_factors_otp:otp-disable' %}"
 | 
					                    <a href="{% url 'passbook_stages_otp:otp-disable' %}"
 | 
				
			||||||
                    class="btn btn-danger btn-sm">{% trans "Disable OTP" %}</a>
 | 
					                    class="btn btn-danger btn-sm">{% trans "Disable OTP" %}</a>
 | 
				
			||||||
                    {% endif %}
 | 
					                    {% endif %}
 | 
				
			||||||
                </p>
 | 
					                </p>
 | 
				
			||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.factors.otp import views
 | 
					from passbook.stages.otp import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("", views.UserSettingsView.as_view(), name="otp-user-settings"),
 | 
					    path("", views.UserSettingsView.as_view(), name="otp-user-settings"),
 | 
				
			||||||
@ -19,12 +19,12 @@ from qrcode.image.svg import SvgPathImage
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.audit.models import Event, EventAction
 | 
					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.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_SESSION_KEY = "passbook_stages_otp_key"
 | 
				
			||||||
OTP_SETTING_UP_KEY = "passbook_factors_otp_setup"
 | 
					OTP_SETTING_UP_KEY = "passbook_stages_otp_setup"
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,7 +33,7 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    template_name = "otp/user_settings.html"
 | 
					    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):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        static = StaticDevice.objects.filter(user=self.request.user, confirmed=True)
 | 
					        static = StaticDevice.objects.filter(user=self.request.user, confirmed=True)
 | 
				
			||||||
@ -61,7 +61,7 @@ class DisableView(LoginRequiredMixin, View):
 | 
				
			|||||||
        messages.success(request, "Successfully disabled OTP")
 | 
					        messages.success(request, "Successfully disabled OTP")
 | 
				
			||||||
        # Create event with email notification
 | 
					        # Create event with email notification
 | 
				
			||||||
        Event.new(EventAction.CUSTOM, message="User disabled OTP.").from_http(request)
 | 
					        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):
 | 
					class EnableView(LoginRequiredMixin, FormView):
 | 
				
			||||||
@ -74,7 +74,7 @@ class EnableView(LoginRequiredMixin, FormView):
 | 
				
			|||||||
    totp_device = None
 | 
					    totp_device = None
 | 
				
			||||||
    static_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):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs["config"] = CONFIG.y("passbook")
 | 
					        kwargs["config"] = CONFIG.y("passbook")
 | 
				
			||||||
        kwargs["title"] = _("Configure OTP")
 | 
					        kwargs["title"] = _("Configure OTP")
 | 
				
			||||||
@ -92,7 +92,7 @@ class EnableView(LoginRequiredMixin, FormView):
 | 
				
			|||||||
        if finished_totp_devices.exists() and finished_static_devices.exists():
 | 
					        if finished_totp_devices.exists() and finished_static_devices.exists():
 | 
				
			||||||
            messages.error(request, _("You already have TOTP enabled!"))
 | 
					            messages.error(request, _("You already have TOTP enabled!"))
 | 
				
			||||||
            del request.session[OTP_SETTING_UP_KEY]
 | 
					            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
 | 
					        request.session[OTP_SETTING_UP_KEY] = True
 | 
				
			||||||
        # Check if there's an unconfirmed device left to set up
 | 
					        # Check if there's an unconfirmed device left to set up
 | 
				
			||||||
        totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
 | 
					        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):
 | 
					    def get_form(self, form_class=None):
 | 
				
			||||||
        form = super().get_form(form_class=form_class)
 | 
					        form = super().get_form(form_class=form_class)
 | 
				
			||||||
        form.device = self.totp_device
 | 
					        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()]
 | 
					        tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
 | 
				
			||||||
        form.fields["tokens"].choices = tokens
 | 
					        form.fields["tokens"].choices = tokens
 | 
				
			||||||
        return form
 | 
					        return form
 | 
				
			||||||
@ -143,7 +143,7 @@ class EnableView(LoginRequiredMixin, FormView):
 | 
				
			|||||||
        Event.new(EventAction.CUSTOM, message="User enabled OTP.").from_http(
 | 
					        Event.new(EventAction.CUSTOM, message="User enabled OTP.").from_http(
 | 
				
			||||||
            self.request
 | 
					            self.request
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return redirect("passbook_factors_otp:otp-user-settings")
 | 
					        return redirect("passbook_stages_otp:otp-user-settings")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@method_decorator(never_cache, name="dispatch")
 | 
					@method_decorator(never_cache, name="dispatch")
 | 
				
			||||||
							
								
								
									
										26
									
								
								passbook/stages/password/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/stages/password/api.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
							
								
								
									
										10
									
								
								passbook/stages/password/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/stages/password/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
				
			||||||
@ -4,9 +4,8 @@ from django.conf import settings
 | 
				
			|||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
					from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					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.lib.utils.reflection import path_to_class
 | 
				
			||||||
 | 
					from passbook.stages.password.models import PasswordStage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_authentication_backends():
 | 
					def get_authentication_backends():
 | 
				
			||||||
@ -32,25 +31,17 @@ class PasswordForm(forms.Form):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PasswordFactorForm(forms.ModelForm):
 | 
					class PasswordStageForm(forms.ModelForm):
 | 
				
			||||||
    """Form to create/edit Password Factors"""
 | 
					    """Form to create/edit Password Stages"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = PasswordFactor
 | 
					        model = PasswordStage
 | 
				
			||||||
        fields = GENERAL_FIELDS + ["backends", "password_policies", "reset_factors"]
 | 
					        fields = ["name", "backends"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
            "order": forms.NumberInput(),
 | 
					 | 
				
			||||||
            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
					 | 
				
			||||||
            "backends": FilteredSelectMultiple(
 | 
					            "backends": FilteredSelectMultiple(
 | 
				
			||||||
                _("backends"), False, choices=get_authentication_backends()
 | 
					                _("backends"), False, choices=get_authentication_backends()
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "password_policies": FilteredSelectMultiple(_("password policies"), False),
 | 
					            "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."
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -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.contrib.postgres.fields
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
@ -10,28 +10,31 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("passbook_core", "0001_initial"),
 | 
					        ("passbook_flows", "0001_initial"),
 | 
				
			||||||
 | 
					        ("passbook_core", "0012_delete_factor"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name="PasswordFactor",
 | 
					            name="PasswordStage",
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "factor_ptr",
 | 
					                    "stage_ptr",
 | 
				
			||||||
                    models.OneToOneField(
 | 
					                    models.OneToOneField(
 | 
				
			||||||
                        auto_created=True,
 | 
					                        auto_created=True,
 | 
				
			||||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
                        parent_link=True,
 | 
					                        parent_link=True,
 | 
				
			||||||
                        primary_key=True,
 | 
					                        primary_key=True,
 | 
				
			||||||
                        serialize=False,
 | 
					                        serialize=False,
 | 
				
			||||||
                        to="passbook_core.Factor",
 | 
					                        to="passbook_flows.Stage",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                (
 | 
					                (
 | 
				
			||||||
                    "backends",
 | 
					                    "backends",
 | 
				
			||||||
                    django.contrib.postgres.fields.ArrayField(
 | 
					                    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={
 | 
					            options={
 | 
				
			||||||
                "verbose_name": "Password Factor",
 | 
					                "verbose_name": "Password Stage",
 | 
				
			||||||
                "verbose_name_plural": "Password Factors",
 | 
					                "verbose_name_plural": "Password Stages",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bases=("passbook_core.factor",),
 | 
					            bases=("passbook_flows.stage",),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
@ -1,26 +1,24 @@
 | 
				
			|||||||
"""password factor models"""
 | 
					"""password stage models"""
 | 
				
			||||||
from django.contrib.postgres.fields import ArrayField
 | 
					from django.contrib.postgres.fields import ArrayField
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					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.core.types import UIUserSettings
 | 
				
			||||||
 | 
					from passbook.flows.models import Stage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PasswordFactor(Factor):
 | 
					class PasswordStage(Stage):
 | 
				
			||||||
    """Password-based Django-backend Authentication Factor"""
 | 
					    """Password-based Django-backend Authentication Stage"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    backends = ArrayField(
 | 
					    backends = ArrayField(
 | 
				
			||||||
        models.TextField(),
 | 
					        models.TextField(),
 | 
				
			||||||
        help_text=_("Selection of backends to test the password against."),
 | 
					        help_text=_("Selection of backends to test the password against."),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    password_policies = models.ManyToManyField(Policy, blank=True)
 | 
					    password_policies = models.ManyToManyField(Policy, blank=True)
 | 
				
			||||||
    reset_factors = models.ManyToManyField(
 | 
					 | 
				
			||||||
        Factor, blank=True, related_name="reset_factors"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    type = "passbook.factors.password.factor.PasswordFactor"
 | 
					    type = "passbook.stages.password.stage.PasswordStage"
 | 
				
			||||||
    form = "passbook.factors.password.forms.PasswordFactorForm"
 | 
					    form = "passbook.stages.password.forms.PasswordStageForm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def ui_user_settings(self) -> UIUserSettings:
 | 
					    def ui_user_settings(self) -> UIUserSettings:
 | 
				
			||||||
@ -38,9 +36,9 @@ class PasswordFactor(Factor):
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return "Password Factor %s" % self.slug
 | 
					        return f"Password Stage {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Password Factor")
 | 
					        verbose_name = _("Password Stage")
 | 
				
			||||||
        verbose_name_plural = _("Password Factors")
 | 
					        verbose_name_plural = _("Password Stages")
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""passbook multi-factor authentication engine"""
 | 
					"""passbook password stage"""
 | 
				
			||||||
from inspect import Signature
 | 
					from inspect import Signature
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,11 +11,11 @@ from django.views.generic import FormView
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from passbook.core.models import User
 | 
					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.planner import PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
 | 
					from passbook.flows.stage import AuthenticationStage
 | 
				
			||||||
from passbook.lib.config import CONFIG
 | 
					from passbook.lib.config import CONFIG
 | 
				
			||||||
from passbook.lib.utils.reflection import path_to_class
 | 
					from passbook.lib.utils.reflection import path_to_class
 | 
				
			||||||
 | 
					from passbook.stages.password.forms import PasswordForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
 | 
					PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
 | 
				
			||||||
@ -53,11 +53,11 @@ def authenticate(request, backends, **credentials) -> Optional[User]:
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PasswordFactor(FormView, AuthenticationFactor):
 | 
					class PasswordStage(FormView, AuthenticationStage):
 | 
				
			||||||
    """Authentication factor which authenticates against django's AuthBackend"""
 | 
					    """Authentication stage which authenticates against django's AuthBackend"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = PasswordForm
 | 
					    form_class = PasswordForm
 | 
				
			||||||
    template_name = "factors/password/backend.html"
 | 
					    template_name = "stages/password/backend.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """Authenticate against django's authentication backend"""
 | 
					        """Authenticate against django's authentication backend"""
 | 
				
			||||||
@ -71,7 +71,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user = authenticate(
 | 
					            user = authenticate(
 | 
				
			||||||
                self.request, self.executor.current_factor.backends, **kwargs
 | 
					                self.request, self.executor.current_stage.backends, **kwargs
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if user:
 | 
					            if user:
 | 
				
			||||||
                # User instance returned from authenticate() has .backend property set
 | 
					                # User instance returned from authenticate() has .backend property set
 | 
				
			||||||
@ -79,7 +79,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
 | 
				
			|||||||
                self.executor.plan.context[
 | 
					                self.executor.plan.context[
 | 
				
			||||||
                    PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
					                    PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
				
			||||||
                ] = user.backend
 | 
					                ] = user.backend
 | 
				
			||||||
                return self.executor.factor_ok()
 | 
					                return self.executor.stage_ok()
 | 
				
			||||||
            # No user was found -> invalid credentials
 | 
					            # No user was found -> invalid credentials
 | 
				
			||||||
            LOGGER.debug("Invalid credentials")
 | 
					            LOGGER.debug("Invalid credentials")
 | 
				
			||||||
            # Manually inject error into form
 | 
					            # Manually inject error into form
 | 
				
			||||||
@ -90,4 +90,4 @@ class PasswordFactor(FormView, AuthenticationFactor):
 | 
				
			|||||||
        except PermissionDenied:
 | 
					        except PermissionDenied:
 | 
				
			||||||
            # User was found, but permission was denied (i.e. user is not active)
 | 
					            # User was found, but permission was denied (i.e. user is not active)
 | 
				
			||||||
            LOGGER.debug("Denied access", **kwargs)
 | 
					            LOGGER.debug("Denied access", **kwargs)
 | 
				
			||||||
            return self.executor.factor_invalid()
 | 
					            return self.executor.stage_invalid()
 | 
				
			||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
#!/bin/bash -xe
 | 
					#!/bin/bash -xe
 | 
				
			||||||
coverage run --concurrency=multiprocessing manage.py test
 | 
					coverage run --concurrency=multiprocessing manage.py test --failfast
 | 
				
			||||||
coverage combine
 | 
					coverage combine
 | 
				
			||||||
coverage html
 | 
					coverage html
 | 
				
			||||||
coverage report
 | 
					coverage report
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user