flows/: more migration progress, consolidate views

This commit is contained in:
Jens Langhammer 2020-05-07 21:30:52 +02:00
parent 8de66b27ad
commit 5400882d78
23 changed files with 77 additions and 48 deletions

View file

@ -4,6 +4,6 @@
{% block beneath_form %} {% block beneath_form %}
{% if show_password_forget_notice %} {% if show_password_forget_notice %}
<a href="{% url 'passbook_core:auth-process' %}?password-forgotten">{% trans 'Forgot password?' %}</a> <a href="{% url 'passbook_flows:auth-process' %}?password-forgotten">{% trans 'Forgot password?' %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -77,7 +77,7 @@ class TestAuthenticationViews(TestCase):
reverse("passbook_core:auth-login"), data=self.login_data reverse("passbook_core:auth-login"), data=self.login_data
) )
self.assertEqual(login_response.status_code, 302) self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response.url, reverse("passbook_core:auth-process")) self.assertEqual(login_response.url, reverse("passbook_flows:auth-process"))
def test_sign_up_view_post(self): def test_sign_up_view_post(self):
"""Test account.sign_up view POST (Anonymous)""" """Test account.sign_up view POST (Anonymous)"""

View file

@ -1,11 +1,7 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from django.urls import path from django.urls import path
from structlog import get_logger
from passbook.core.views import authentication, overview, user from passbook.core.views import authentication, overview, user
from passbook.factors import view
LOGGER = get_logger()
urlpatterns = [ urlpatterns = [
# Authentication views # Authentication views
@ -17,22 +13,11 @@ urlpatterns = [
authentication.SignUpConfirmView.as_view(), authentication.SignUpConfirmView.as_view(),
name="auth-sign-up-confirm", name="auth-sign-up-confirm",
), ),
path(
"auth/process/denied/",
view.FactorPermissionDeniedView.as_view(),
name="auth-denied",
),
path( path(
"auth/password/reset/<uuid:nonce>/", "auth/password/reset/<uuid:nonce>/",
authentication.PasswordResetView.as_view(), authentication.PasswordResetView.as_view(),
name="auth-password-reset", name="auth-password-reset",
), ),
path("auth/process/", view.AuthenticationView.as_view(), name="auth-process"),
path(
"auth/process/<slug:factor>/",
view.AuthenticationView.as_view(),
name="auth-process",
),
# User views # User views
path("_/user/", user.UserSettingsView.as_view(), name="user-settings"), path("_/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("_/user/delete/", user.UserDeleteView.as_view(), name="user-delete"), path("_/user/delete/", user.UserDeleteView.as_view(), name="user-delete"),

View file

@ -16,7 +16,7 @@ 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.factors.password.exceptions import PasswordPolicyInvalid
from passbook.factors.view import AuthenticationView, _redirect_with_qs from passbook.flows.view import AuthenticationView, _redirect_with_qs
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()
@ -72,7 +72,7 @@ class LoginView(UserPassesTestMixin, FormView):
# No user found # No user found
return self.invalid_login(self.request) return self.invalid_login(self.request)
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs("passbook_core:auth-process", self.request.GET) return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
def invalid_login( def invalid_login(
self, request: HttpRequest, disabled_user: User = None self, request: HttpRequest, disabled_user: User = None

View file

@ -2,8 +2,8 @@
from django.views.generic import FormView from django.views.generic import FormView
from passbook.factors.base import AuthenticationFactor
from passbook.factors.captcha.forms import CaptchaForm from passbook.factors.captcha.forms import CaptchaForm
from passbook.flows.factor_base import AuthenticationFactor
class CaptchaFactor(FormView, AuthenticationFactor): class CaptchaFactor(FormView, AuthenticationFactor):

View file

@ -5,7 +5,7 @@ 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.captcha.models import CaptchaFactor from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS from passbook.flows.forms import GENERAL_FIELDS
class CaptchaForm(forms.Form): class CaptchaForm(forms.Form):

View file

@ -1,7 +1,7 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from django.http import HttpRequest from django.http import HttpRequest
from passbook.factors.base import AuthenticationFactor from passbook.flows.factor_base import AuthenticationFactor
class DummyFactor(AuthenticationFactor): class DummyFactor(AuthenticationFactor):

View file

@ -4,7 +4,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.factors.dummy.models import DummyFactor from passbook.factors.dummy.models import DummyFactor
from passbook.factors.forms import GENERAL_FIELDS from passbook.flows.forms import GENERAL_FIELDS
class DummyFactorForm(forms.ModelForm): class DummyFactorForm(forms.ModelForm):

View file

@ -6,9 +6,9 @@ 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.base import AuthenticationFactor
from passbook.factors.email.tasks import send_mails from passbook.factors.email.tasks import send_mails
from passbook.factors.email.utils import TemplateEmailMessage from passbook.factors.email.utils import TemplateEmailMessage
from passbook.flows.factor_base import AuthenticationFactor
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()

View file

@ -4,7 +4,7 @@ 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.factors.email.models import EmailFactor
from passbook.factors.forms import GENERAL_FIELDS from passbook.flows.forms import GENERAL_FIELDS
class EmailFactorForm(forms.ModelForm): class EmailFactorForm(forms.ModelForm):

View file

@ -5,9 +5,9 @@ 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.base import AuthenticationFactor
from passbook.factors.otp.forms import OTPVerifyForm from passbook.factors.otp.forms import OTPVerifyForm
from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView
from passbook.flows.factor_base import AuthenticationFactor
LOGGER = get_logger() LOGGER = get_logger()

View file

@ -7,8 +7,8 @@ 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.forms import GENERAL_FIELDS
from passbook.factors.otp.models import OTPFactor from passbook.factors.otp.models import OTPFactor
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.")

View file

@ -11,9 +11,9 @@ 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.base import AuthenticationFactor
from passbook.factors.password.forms import PasswordForm from passbook.factors.password.forms import PasswordForm
from passbook.factors.view import AuthenticationView from passbook.flows.factor_base import AuthenticationFactor
from passbook.flows.view import AuthenticationView
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

View file

@ -4,8 +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.forms import GENERAL_FIELDS
from passbook.factors.password.models import PasswordFactor 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

View file

@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.core.models import User from passbook.core.models import User
from passbook.factors.view import AuthenticationView from passbook.flows.view import AuthenticationView
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG

View file

@ -0,0 +1,21 @@
# 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.",
),
),
]

View file

@ -59,6 +59,13 @@ class FlowFactorBinding(PolicyBindingModel, UUIDModel):
flow = models.ForeignKey("Flow", on_delete=models.CASCADE) flow = models.ForeignKey("Flow", on_delete=models.CASCADE)
factor = models.ForeignKey(Factor, on_delete=models.CASCADE) factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
re_evaluate_policies = models.BooleanField(
default=False,
help_text=_(
"When this option is enabled, the planner will re-evaluate policies bound to this."
),
)
order = models.IntegerField() order = models.IntegerField()
def __str__(self) -> str: def __str__(self) -> str:

View file

@ -10,7 +10,7 @@ from django.urls import reverse
from passbook.core.models import User from passbook.core.models import User
from passbook.factors.dummy.models import DummyFactor from passbook.factors.dummy.models import DummyFactor
from passbook.factors.password.models import PasswordFactor from passbook.factors.password.models import PasswordFactor
from passbook.factors.view import AuthenticationView from passbook.flows.view import AuthenticationView
class TestFactorAuthentication(TestCase): class TestFactorAuthentication(TestCase):
@ -37,13 +37,13 @@ class TestFactorAuthentication(TestCase):
def test_unauthenticated_raw(self): def test_unauthenticated_raw(self):
"""test direct call to AuthenticationView""" """test direct call to AuthenticationView"""
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_flows:auth-process"))
# Response should be 400 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_unauthenticated_prepared(self): def test_unauthenticated_prepared(self):
"""test direct call but with pending_uesr in session""" """test direct call but with pending_uesr in session"""
request = RequestFactory().get(reverse("passbook_core:auth-process")) request = RequestFactory().get(reverse("passbook_flows:auth-process"))
request.user = AnonymousUser() request.user = AnonymousUser()
request.session = {} request.session = {}
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
@ -55,21 +55,21 @@ class TestFactorAuthentication(TestCase):
"""Test with all factors disabled""" """Test with all factors disabled"""
self.factor.enabled = False self.factor.enabled = False
self.factor.save() self.factor.save()
request = RequestFactory().get(reverse("passbook_core:auth-process")) request = RequestFactory().get(reverse("passbook_flows:auth-process"))
request.user = AnonymousUser() request.user = AnonymousUser()
request.session = {} request.session = {}
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request) response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("passbook_core:auth-denied")) self.assertEqual(response.url, reverse("passbook_flows:auth-denied"))
self.factor.enabled = True self.factor.enabled = True
self.factor.save() self.factor.save()
def test_authenticated(self): def test_authenticated(self):
"""Test with already logged in user""" """Test with already logged in user"""
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_flows:auth-process"))
# Response should be 400 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.client.logout() self.client.logout()
@ -77,7 +77,7 @@ class TestFactorAuthentication(TestCase):
def test_unauthenticated_post(self): def test_unauthenticated_post(self):
"""Test post request as unauthenticated user""" """Test post request as unauthenticated user"""
request = RequestFactory().post( request = RequestFactory().post(
reverse("passbook_core:auth-process"), data={"password": self.password} reverse("passbook_flows:auth-process"), data={"password": self.password}
) )
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
@ -93,7 +93,7 @@ class TestFactorAuthentication(TestCase):
def test_unauthenticated_post_invalid(self): def test_unauthenticated_post_invalid(self):
"""Test post request as unauthenticated user""" """Test post request as unauthenticated user"""
request = RequestFactory().post( request = RequestFactory().post(
reverse("passbook_core:auth-process"), reverse("passbook_flows:auth-process"),
data={"password": self.password + "a"}, data={"password": self.password + "a"},
) )
request.user = AnonymousUser() request.user = AnonymousUser()
@ -110,7 +110,7 @@ class TestFactorAuthentication(TestCase):
"""Test view with multiple active factors""" """Test view with multiple active factors"""
DummyFactor.objects.get_or_create(name="dummy", slug="dummy", order=1) DummyFactor.objects.get_or_create(name="dummy", slug="dummy", order=1)
request = RequestFactory().post( request = RequestFactory().post(
reverse("passbook_core:auth-process"), data={"password": self.password} reverse("passbook_flows:auth-process"), data={"password": self.password}
) )
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
@ -122,10 +122,10 @@ class TestFactorAuthentication(TestCase):
session_copy = request.session.items() session_copy = request.session.items()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# Verify view redirects to itself after auth # Verify view redirects to itself after auth
self.assertEqual(response.url, reverse("passbook_core:auth-process")) self.assertEqual(response.url, reverse("passbook_flows:auth-process"))
# Run another request with same session which should result in a logged in user # Run another request with same session which should result in a logged in user
request = RequestFactory().post(reverse("passbook_core:auth-process")) request = RequestFactory().post(reverse("passbook_flows:auth-process"))
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
middleware.process_request(request) middleware.process_request(request)

View file

@ -1,2 +1,18 @@
"""flow urls""" """flow urls"""
urlpatterns = [] from django.urls import path
from passbook.flows.view import AuthenticationView, FactorPermissionDeniedView
urlpatterns = [
path("auth/process/", AuthenticationView.as_view(), name="auth-process"),
path(
"auth/process/<slug:factor>/",
AuthenticationView.as_view(),
name="auth-process",
),
path(
"auth/process/denied/",
FactorPermissionDeniedView.as_view(),
name="auth-denied",
),
]

View file

@ -178,7 +178,7 @@ class AuthenticationView(UserPassesTestMixin, View):
] = self.pending_factors ] = self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor", next_factor=next_factor) LOGGER.debug("Rendering Factor", next_factor=next_factor)
return _redirect_with_qs("passbook_core:auth-process", self.request.GET) return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
# User passed all factors # User passed all factors
LOGGER.debug("User passed all factors, logging in", user=self.pending_user) LOGGER.debug("User passed all factors, logging in", user=self.pending_user)
return self._user_passed() return self._user_passed()
@ -188,7 +188,7 @@ class AuthenticationView(UserPassesTestMixin, View):
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
self.cleanup() self.cleanup()
return _redirect_with_qs("passbook_core:auth-denied", self.request.GET) return _redirect_with_qs("passbook_flows:auth-denied", self.request.GET)
def _user_passed(self) -> HttpResponse: def _user_passed(self) -> HttpResponse:
"""User Successfully passed all factors""" """User Successfully passed all factors"""

View file

@ -8,7 +8,7 @@ from jinja2.exceptions import TemplateSyntaxError, UndefinedError
from jinja2.nativetypes import NativeEnvironment from jinja2.nativetypes import NativeEnvironment
from structlog import get_logger from structlog import get_logger
from passbook.factors.view import AuthenticationView from passbook.flows.view import AuthenticationView
from passbook.lib.utils.http import get_client_ip from passbook.lib.utils.http import get_client_ip
from passbook.policies.types import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult

View file

@ -13,7 +13,7 @@ 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.view import AuthenticationView, _redirect_with_qs from passbook.flows.view import AuthenticationView, _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
@ -168,7 +168,7 @@ class OAuthCallback(OAuthClientMixin, View):
self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True
return _redirect_with_qs("passbook_core:auth-process", self.request.GET) return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info): def handle_existing_user(self, source, user, access, info):