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