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