flows: complete migration to FlowExecutorView, fully use context
This commit is contained in:
parent
114bb1b0bd
commit
2a85e5ae87
|
@ -16,8 +16,11 @@ 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.flows.views import AuthenticationView, _redirect_with_qs
|
from passbook.flows.models import Flow, FlowDesignation
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
from passbook.lib.utils.urls import redirect_with_qs
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -71,8 +74,15 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||||
if not pre_user:
|
if not pre_user:
|
||||||
# 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
|
# We run the Flow planner here so we can pass the Pending user in the context
|
||||||
return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
|
flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
|
||||||
|
planner = FlowPlanner(flow)
|
||||||
|
plan = planner.plan(self.request)
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = pre_user
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"passbook_flows:flow-executor", self.request.GET, flow_slug=flow.slug,
|
||||||
|
)
|
||||||
|
|
||||||
def invalid_login(
|
def invalid_login(
|
||||||
self, request: HttpRequest, disabled_user: User = None
|
self, request: HttpRequest, disabled_user: User = None
|
||||||
|
|
|
@ -12,14 +12,12 @@ class CaptchaFactor(FormView, AuthenticationFactor):
|
||||||
form_class = CaptchaForm
|
form_class = CaptchaForm
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
return self.authenticator.user_ok()
|
return self.executor.factor_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.authenticator.current_factor.public_key
|
form.fields["captcha"].public_key = self.executor.current_factor.public_key
|
||||||
form.fields[
|
form.fields["captcha"].private_key = self.executor.current_factor.private_key
|
||||||
"captcha"
|
|
||||||
].private_key = self.authenticator.current_factor.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
|
||||||
|
|
|
@ -9,4 +9,4 @@ class DummyFactor(AuthenticationFactor):
|
||||||
|
|
||||||
def post(self, request: HttpRequest):
|
def post(self, request: HttpRequest):
|
||||||
"""Just redirect to next factor"""
|
"""Just redirect to next factor"""
|
||||||
return self.authenticator.user_ok()
|
return self.executor.factor_ok()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""passbook multi-factor authentication engine"""
|
"""passbook multi-factor 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 redirect, reverse
|
from django.shortcuts import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from passbook.core.models import Nonce
|
||||||
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.flows.factor_base import AuthenticationFactor
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -24,12 +25,13 @@ class EmailFactorView(AuthenticationFactor):
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
nonce = Nonce.objects.create(user=self.pending_user)
|
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
nonce = Nonce.objects.create(user=pending_user)
|
||||||
# Send mail to user
|
# Send mail to user
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_("Forgotten password"),
|
subject=_("Forgotten password"),
|
||||||
template_name="email/account_password_reset.html",
|
template_name="email/account_password_reset.html",
|
||||||
to=[self.pending_user.email],
|
to=[pending_user.email],
|
||||||
template_context={
|
template_context={
|
||||||
"url": self.request.build_absolute_uri(
|
"url": self.request.build_absolute_uri(
|
||||||
reverse(
|
reverse(
|
||||||
|
@ -39,11 +41,10 @@ class EmailFactorView(AuthenticationFactor):
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
send_mails(self.authenticator.current_factor, message)
|
send_mails(self.executor.current_factor, message)
|
||||||
self.authenticator.cleanup()
|
|
||||||
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 redirect("passbook_core:auth-login")
|
return self.executor.cancel()
|
||||||
|
|
||||||
def post(self, request: HttpRequest):
|
def post(self, request: HttpRequest):
|
||||||
"""Just redirect to next factor"""
|
"""Just redirect to next factor"""
|
||||||
return self.authenticator.user_ok()
|
return self.executor.factor_ok()
|
||||||
|
|
|
@ -8,6 +8,7 @@ from structlog import get_logger
|
||||||
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
|
from passbook.flows.factor_base import AuthenticationFactor
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -25,31 +26,34 @@ class OTPFactor(FormView, AuthenticationFactor):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Check if User has OTP enabled and if OTP is enforced"""
|
"""Check if User has OTP enabled and if OTP is enforced"""
|
||||||
if not user_has_device(self.pending_user):
|
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
if not user_has_device(pending_user):
|
||||||
LOGGER.debug("User doesn't have OTP Setup.")
|
LOGGER.debug("User doesn't have OTP Setup.")
|
||||||
if self.authenticator.current_factor.enforced:
|
if self.executor.current_factor.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 = self.pending_user
|
request.user = pending_user
|
||||||
LOGGER.debug("Passing GET to EnableView")
|
|
||||||
messages.info(request, _("OTP is enforced. Please setup OTP."))
|
messages.info(request, _("OTP is enforced. Please setup OTP."))
|
||||||
return EnableView.as_view()(request)
|
return EnableView.as_view()(request)
|
||||||
LOGGER.debug("OTP is not enforced, skipping form")
|
LOGGER.debug("OTP is not enforced, skipping form")
|
||||||
return self.authenticator.user_ok()
|
return self.executor.user_ok()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Check if setup is in progress and redirect to EnableView"""
|
"""Check if setup is in progress and redirect to EnableView"""
|
||||||
if OTP_SETTING_UP_KEY in request.session:
|
if OTP_SETTING_UP_KEY in request.session:
|
||||||
LOGGER.debug("Passing POST to EnableView")
|
LOGGER.debug("Passing POST to EnableView")
|
||||||
request.user = self.pending_user
|
request.user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
return EnableView.as_view()(request)
|
return EnableView.as_view()(request)
|
||||||
return super().post(self, request, *args, **kwargs)
|
return super().post(self, request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form: OTPVerifyForm):
|
def form_valid(self, form: OTPVerifyForm):
|
||||||
"""Verify OTP Token"""
|
"""Verify OTP Token"""
|
||||||
device = match_token(self.pending_user, form.cleaned_data.get("code"))
|
device = match_token(
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
|
||||||
|
form.cleaned_data.get("code"),
|
||||||
|
)
|
||||||
if device:
|
if device:
|
||||||
return self.authenticator.user_ok()
|
return self.executor.factor_ok()
|
||||||
messages.error(self.request, _("Invalid OTP."))
|
messages.error(self.request, _("Invalid OTP."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
|
@ -13,11 +13,12 @@ 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.factors.password.forms import PasswordForm
|
||||||
from passbook.flows.factor_base import AuthenticationFactor
|
from passbook.flows.factor_base import AuthenticationFactor
|
||||||
from passbook.flows.views import AuthenticationView
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
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
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
||||||
|
|
||||||
|
|
||||||
def authenticate(request, backends, **credentials) -> Optional[User]:
|
def authenticate(request, backends, **credentials) -> Optional[User]:
|
||||||
|
@ -56,7 +57,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
|
||||||
"""Authentication factor which authenticates against django's AuthBackend"""
|
"""Authentication factor which authenticates against django's AuthBackend"""
|
||||||
|
|
||||||
form_class = PasswordForm
|
form_class = PasswordForm
|
||||||
template_name = "login/factors/backend.html"
|
template_name = "factors/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"""
|
||||||
|
@ -65,18 +66,20 @@ class PasswordFactor(FormView, AuthenticationFactor):
|
||||||
"password": form.cleaned_data.get("password"),
|
"password": form.cleaned_data.get("password"),
|
||||||
}
|
}
|
||||||
for uid_field in uid_fields:
|
for uid_field in uid_fields:
|
||||||
kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field)
|
kwargs[uid_field] = getattr(
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER], uid_field
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
self.request, self.authenticator.current_factor.backends, **kwargs
|
self.request, self.executor.current_factor.backends, **kwargs
|
||||||
)
|
)
|
||||||
if user:
|
if user:
|
||||||
# User instance returned from authenticate() has .backend property set
|
# User instance returned from authenticate() has .backend property set
|
||||||
self.authenticator.pending_user = user
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||||
self.request.session[
|
self.executor.plan.context[
|
||||||
AuthenticationView.SESSION_USER_BACKEND
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
] = user.backend
|
] = user.backend
|
||||||
return self.authenticator.user_ok()
|
return self.executor.factor_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
|
||||||
|
@ -87,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.authenticator.user_invalid()
|
return self.executor.factor_invalid()
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
"""passbook multi-factor authentication engine"""
|
"""passbook multi-factor authentication engine"""
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext as _
|
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.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.flows.views import AuthenticationView
|
from passbook.flows.views import FlowExecutorView
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,19 +15,19 @@ class AuthenticationFactor(TemplateView):
|
||||||
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
|
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
|
||||||
|
|
||||||
form: ModelForm = None
|
form: ModelForm = None
|
||||||
required: bool = True
|
|
||||||
authenticator: AuthenticationView
|
executor: FlowExecutorView
|
||||||
pending_user: User
|
|
||||||
request: HttpRequest = None
|
request: HttpRequest = None
|
||||||
template_name = "login/form_with_user.html"
|
template_name = "login/form_with_user.html"
|
||||||
|
|
||||||
def __init__(self, authenticator: AuthenticationView):
|
def __init__(self, executor: FlowExecutorView):
|
||||||
self.authenticator = authenticator
|
self.executor = executor
|
||||||
self.pending_user = None
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
kwargs["config"] = CONFIG.y("passbook")
|
kwargs["config"] = CONFIG.y("passbook")
|
||||||
kwargs["title"] = _("Log in to your account")
|
kwargs["title"] = _("Log in to your account")
|
||||||
kwargs["primary_action"] = _("Log in")
|
kwargs["primary_action"] = _("Log in")
|
||||||
kwargs["user"] = self.pending_user
|
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
||||||
|
kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Flows Planner"""
|
"""Flows Planner"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from time import time
|
from time import time
|
||||||
from typing import List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
@ -12,6 +12,9 @@ from passbook.policies.engine import PolicyEngine
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
PLAN_CONTEXT_PENDING_USER = "pending_user"
|
||||||
|
PLAN_CONTEXT_SSO = "is_sso"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FlowPlan:
|
class FlowPlan:
|
||||||
|
@ -19,6 +22,7 @@ class FlowPlan:
|
||||||
of all Factors that should be run."""
|
of all Factors that should be run."""
|
||||||
|
|
||||||
factors: List[Factor] = field(default_factory=list)
|
factors: List[Factor] = field(default_factory=list)
|
||||||
|
context: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def next(self) -> Factor:
|
def next(self) -> Factor:
|
||||||
"""Return next pending factor from the bottom of the list"""
|
"""Return next pending factor from the bottom of the list"""
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
"""passbook Core Authentication Test"""
|
|
||||||
import string
|
|
||||||
from random import SystemRandom
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.test import RequestFactory, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from passbook.core.models import User
|
|
||||||
from passbook.factors.dummy.models import DummyFactor
|
|
||||||
from passbook.factors.password.models import PasswordFactor
|
|
||||||
from passbook.flows.views import AuthenticationView
|
|
||||||
|
|
||||||
|
|
||||||
class TestFactorAuthentication(TestCase):
|
|
||||||
"""passbook Core Authentication Test"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.password = "".join(
|
|
||||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
|
||||||
for _ in range(8)
|
|
||||||
)
|
|
||||||
self.factor, _ = PasswordFactor.objects.get_or_create(
|
|
||||||
slug="password",
|
|
||||||
defaults={
|
|
||||||
"name": "password",
|
|
||||||
"slug": "password",
|
|
||||||
"order": 0,
|
|
||||||
"backends": ["django.contrib.auth.backends.ModelBackend"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.user = User.objects.create_user(
|
|
||||||
username="test", email="test@test.test", password=self.password
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_unauthenticated_raw(self):
|
|
||||||
"""test direct call to AuthenticationView"""
|
|
||||||
response = self.client.get(reverse("passbook_flows:auth-process"))
|
|
||||||
# Response should be 400 since no pending user is set
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
|
|
||||||
def test_unauthenticated_prepared(self):
|
|
||||||
"""test direct call but with pending_uesr in session"""
|
|
||||||
request = RequestFactory().get(reverse("passbook_flows:auth-process"))
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
request.session = {}
|
|
||||||
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
|
||||||
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_no_factors(self):
|
|
||||||
"""Test with all factors disabled"""
|
|
||||||
self.factor.enabled = False
|
|
||||||
self.factor.save()
|
|
||||||
request = RequestFactory().get(reverse("passbook_flows:auth-process"))
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
request.session = {}
|
|
||||||
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
|
||||||
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, reverse("passbook_flows:auth-denied"))
|
|
||||||
self.factor.enabled = True
|
|
||||||
self.factor.save()
|
|
||||||
|
|
||||||
def test_authenticated(self):
|
|
||||||
"""Test with already logged in user"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
response = self.client.get(reverse("passbook_flows:auth-process"))
|
|
||||||
# Response should be 400 since no pending user is set
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.client.logout()
|
|
||||||
|
|
||||||
def test_unauthenticated_post(self):
|
|
||||||
"""Test post request as unauthenticated user"""
|
|
||||||
request = RequestFactory().post(
|
|
||||||
reverse("passbook_flows:auth-process"), data={"password": self.password}
|
|
||||||
)
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware()
|
|
||||||
middleware.process_request(request)
|
|
||||||
request.session.save() # pylint: disable=no-member
|
|
||||||
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
|
||||||
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, reverse("passbook_core:overview"))
|
|
||||||
self.client.logout()
|
|
||||||
|
|
||||||
def test_unauthenticated_post_invalid(self):
|
|
||||||
"""Test post request as unauthenticated user"""
|
|
||||||
request = RequestFactory().post(
|
|
||||||
reverse("passbook_flows:auth-process"),
|
|
||||||
data={"password": self.password + "a"},
|
|
||||||
)
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware()
|
|
||||||
middleware.process_request(request)
|
|
||||||
request.session.save() # pylint: disable=no-member
|
|
||||||
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
|
||||||
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.client.logout()
|
|
||||||
|
|
||||||
def test_multifactor(self):
|
|
||||||
"""Test view with multiple active factors"""
|
|
||||||
DummyFactor.objects.get_or_create(name="dummy", slug="dummy", order=1)
|
|
||||||
request = RequestFactory().post(
|
|
||||||
reverse("passbook_flows:auth-process"), data={"password": self.password}
|
|
||||||
)
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware()
|
|
||||||
middleware.process_request(request)
|
|
||||||
request.session.save() # pylint: disable=no-member
|
|
||||||
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
|
||||||
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
session_copy = request.session.items()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
# Verify view redirects to itself after auth
|
|
||||||
self.assertEqual(response.url, reverse("passbook_flows:auth-process"))
|
|
||||||
|
|
||||||
# Run another request with same session which should result in a logged in user
|
|
||||||
request = RequestFactory().post(reverse("passbook_flows:auth-process"))
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware()
|
|
||||||
middleware.process_request(request)
|
|
||||||
for key, value in session_copy:
|
|
||||||
request.session[key] = value
|
|
||||||
request.session.save() # pylint: disable=no-member
|
|
||||||
response = AuthenticationView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, reverse("passbook_core:overview"))
|
|
|
@ -1,23 +1,8 @@
|
||||||
"""flow urls"""
|
"""flow urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from passbook.flows.views import (
|
from passbook.flows.views import FlowExecutorView
|
||||||
AuthenticationView,
|
|
||||||
FactorPermissionDeniedView,
|
|
||||||
FlowExecutorView,
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
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",
|
|
||||||
),
|
|
||||||
path("<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
path("<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,228 +1,24 @@
|
||||||
"""passbook multi-factor authentication engine"""
|
"""passbook multi-factor authentication engine"""
|
||||||
from typing import List, Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.utils.http import urlencode
|
|
||||||
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, User
|
from passbook.core.models import Factor
|
||||||
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
|
||||||
from passbook.flows.planner import 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
|
||||||
from passbook.lib.utils.urls import is_url_absolute
|
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from passbook.lib.views import bad_request_message
|
from passbook.lib.views import bad_request_message
|
||||||
from passbook.policies.engine import PolicyEngine
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
NEXT_ARG_NAME = "next"
|
NEXT_ARG_NAME = "next"
|
||||||
|
|
||||||
|
|
||||||
def _redirect_with_qs(view, get_query_set=None):
|
|
||||||
"""Wrapper to redirect whilst keeping GET Parameters"""
|
|
||||||
target = reverse(view)
|
|
||||||
if get_query_set:
|
|
||||||
target += "?" + urlencode(get_query_set.items())
|
|
||||||
return redirect(target)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationView(UserPassesTestMixin, View):
|
|
||||||
"""Wizard-like Multi-factor authenticator"""
|
|
||||||
|
|
||||||
SESSION_FACTOR = "passbook_factor"
|
|
||||||
SESSION_PENDING_FACTORS = "passbook_pending_factors"
|
|
||||||
SESSION_PENDING_USER = "passbook_pending_user"
|
|
||||||
SESSION_USER_BACKEND = "passbook_user_backend"
|
|
||||||
SESSION_IS_SSO_LOGIN = "passbook_sso_login"
|
|
||||||
|
|
||||||
pending_user: User
|
|
||||||
pending_factors: List[Tuple[str, str]] = []
|
|
||||||
|
|
||||||
_current_factor_class: Factor
|
|
||||||
|
|
||||||
current_factor: Factor
|
|
||||||
|
|
||||||
# Allow only not authenticated users to login
|
|
||||||
def test_func(self) -> bool:
|
|
||||||
return AuthenticationView.SESSION_PENDING_USER in self.request.session
|
|
||||||
|
|
||||||
def _check_config_domain(self) -> Optional[HttpResponse]:
|
|
||||||
"""Checks if current request's domain matches configured Domain, and
|
|
||||||
adds a warning if not."""
|
|
||||||
current_domain = self.request.get_host()
|
|
||||||
if ":" in current_domain:
|
|
||||||
current_domain, _ = current_domain.split(":")
|
|
||||||
config_domain = CONFIG.y("domain")
|
|
||||||
if current_domain != config_domain:
|
|
||||||
message = (
|
|
||||||
f"Current domain of '{current_domain}' doesn't "
|
|
||||||
f"match configured domain of '{config_domain}'."
|
|
||||||
)
|
|
||||||
LOGGER.warning(message)
|
|
||||||
return bad_request_message(self.request, message)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponse:
|
|
||||||
# Function from UserPassesTestMixin
|
|
||||||
if NEXT_ARG_NAME in self.request.GET:
|
|
||||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
return _redirect_with_qs("passbook_core:overview", self.request.GET)
|
|
||||||
return _redirect_with_qs("passbook_core:auth-login", self.request.GET)
|
|
||||||
|
|
||||||
def get_pending_factors(self) -> List[Tuple[str, str]]:
|
|
||||||
"""Loading pending factors from Database or load from session variable"""
|
|
||||||
# Write pending factors to session
|
|
||||||
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
|
|
||||||
return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS]
|
|
||||||
# Get an initial list of factors which are currently enabled
|
|
||||||
# and apply to the current user. We check policies here and block the request
|
|
||||||
_all_factors = (
|
|
||||||
Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
|
|
||||||
)
|
|
||||||
pending_factors = []
|
|
||||||
for factor in _all_factors:
|
|
||||||
factor: Factor
|
|
||||||
LOGGER.debug(
|
|
||||||
"Checking if factor applies to user",
|
|
||||||
factor=factor,
|
|
||||||
user=self.pending_user,
|
|
||||||
)
|
|
||||||
policy_engine = PolicyEngine(
|
|
||||||
factor.policies.all(), self.pending_user, self.request
|
|
||||||
)
|
|
||||||
policy_engine.build()
|
|
||||||
if policy_engine.passing:
|
|
||||||
pending_factors.append((factor.uuid.hex, factor.type))
|
|
||||||
LOGGER.debug("Factor applies", factor=factor, user=self.pending_user)
|
|
||||||
return pending_factors
|
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
|
|
||||||
user_test_result = self.get_test_func()()
|
|
||||||
if not user_test_result:
|
|
||||||
incorrect_domain_message = self._check_config_domain()
|
|
||||||
if incorrect_domain_message:
|
|
||||||
return incorrect_domain_message
|
|
||||||
return self.handle_no_permission()
|
|
||||||
# Extract pending user from session (only remember uid)
|
|
||||||
self.pending_user = get_object_or_404(
|
|
||||||
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER]
|
|
||||||
)
|
|
||||||
self.pending_factors = self.get_pending_factors()
|
|
||||||
# Read and instantiate factor from session
|
|
||||||
factor_uuid, factor_class = None, None
|
|
||||||
if AuthenticationView.SESSION_FACTOR not in request.session:
|
|
||||||
# Case when no factors apply to user, return error denied
|
|
||||||
if not self.pending_factors:
|
|
||||||
# Case when user logged in from SSO provider and no more factors apply
|
|
||||||
if AuthenticationView.SESSION_IS_SSO_LOGIN in request.session:
|
|
||||||
LOGGER.debug("User authenticated with SSO, logging in...")
|
|
||||||
return self._user_passed()
|
|
||||||
return self.user_invalid()
|
|
||||||
factor_uuid, factor_class = self.pending_factors[0]
|
|
||||||
else:
|
|
||||||
factor_uuid, factor_class = request.session[
|
|
||||||
AuthenticationView.SESSION_FACTOR
|
|
||||||
]
|
|
||||||
# Lookup current factor object
|
|
||||||
self.current_factor = (
|
|
||||||
Factor.objects.filter(uuid=factor_uuid).select_subclasses().first()
|
|
||||||
)
|
|
||||||
# Instantiate Next Factor and pass request
|
|
||||||
factor = path_to_class(factor_class)
|
|
||||||
self._current_factor_class = factor(self)
|
|
||||||
self._current_factor_class.pending_user = self.pending_user
|
|
||||||
self._current_factor_class.request = request
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""pass get request to current factor"""
|
|
||||||
LOGGER.debug(
|
|
||||||
"Passing GET",
|
|
||||||
view_class=class_to_path(self._current_factor_class.__class__),
|
|
||||||
)
|
|
||||||
return self._current_factor_class.get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""pass post request to current factor"""
|
|
||||||
LOGGER.debug(
|
|
||||||
"Passing POST",
|
|
||||||
view_class=class_to_path(self._current_factor_class.__class__),
|
|
||||||
)
|
|
||||||
return self._current_factor_class.post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def user_ok(self) -> HttpResponse:
|
|
||||||
"""Redirect to next Factor"""
|
|
||||||
LOGGER.debug(
|
|
||||||
"Factor passed",
|
|
||||||
factor_class=class_to_path(self._current_factor_class.__class__),
|
|
||||||
)
|
|
||||||
# Remove passed factor from pending factors
|
|
||||||
current_factor_tuple = (
|
|
||||||
self.current_factor.uuid.hex,
|
|
||||||
class_to_path(self._current_factor_class.__class__),
|
|
||||||
)
|
|
||||||
if current_factor_tuple in self.pending_factors:
|
|
||||||
self.pending_factors.remove(current_factor_tuple)
|
|
||||||
next_factor = None
|
|
||||||
if self.pending_factors:
|
|
||||||
next_factor = self.pending_factors.pop()
|
|
||||||
# Save updated pening_factor list to session
|
|
||||||
self.request.session[
|
|
||||||
AuthenticationView.SESSION_PENDING_FACTORS
|
|
||||||
] = self.pending_factors
|
|
||||||
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
|
||||||
LOGGER.debug("Rendering Factor", next_factor=next_factor)
|
|
||||||
return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
|
|
||||||
# User passed all factors
|
|
||||||
LOGGER.debug("User passed all factors, logging in", user=self.pending_user)
|
|
||||||
return self._user_passed()
|
|
||||||
|
|
||||||
def user_invalid(self) -> HttpResponse:
|
|
||||||
"""Show error message, user cannot login.
|
|
||||||
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
|
||||||
LOGGER.debug("User invalid")
|
|
||||||
self.cleanup()
|
|
||||||
return _redirect_with_qs("passbook_flows:auth-denied", self.request.GET)
|
|
||||||
|
|
||||||
def _user_passed(self) -> HttpResponse:
|
|
||||||
"""User Successfully passed all factors"""
|
|
||||||
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
|
||||||
login(self.request, self.pending_user, backend=backend)
|
|
||||||
LOGGER.debug("Logged in", user=self.pending_user)
|
|
||||||
# Cleanup
|
|
||||||
self.cleanup()
|
|
||||||
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
|
|
||||||
if next_param and not is_url_absolute(next_param):
|
|
||||||
return redirect(next_param)
|
|
||||||
return _redirect_with_qs("passbook_core:overview")
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Remove temporary data from session"""
|
|
||||||
session_keys = [
|
|
||||||
self.SESSION_FACTOR,
|
|
||||||
self.SESSION_PENDING_FACTORS,
|
|
||||||
self.SESSION_PENDING_USER,
|
|
||||||
self.SESSION_USER_BACKEND,
|
|
||||||
]
|
|
||||||
for key in session_keys:
|
|
||||||
if key in self.request.session:
|
|
||||||
del self.request.session[key]
|
|
||||||
LOGGER.debug("Cleaned up sessions")
|
|
||||||
|
|
||||||
|
|
||||||
class FactorPermissionDeniedView(PermissionDeniedView):
|
|
||||||
"""User could not be authenticated"""
|
|
||||||
|
|
||||||
|
|
||||||
SESSION_KEY_PLAN = "passbook_flows_plan"
|
SESSION_KEY_PLAN = "passbook_flows_plan"
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,6 +36,32 @@ class FlowExecutorView(View):
|
||||||
# TODO: Do we always need this?
|
# TODO: Do we always need this?
|
||||||
self.flow = get_object_or_404(Flow, slug=flow_slug)
|
self.flow = get_object_or_404(Flow, slug=flow_slug)
|
||||||
|
|
||||||
|
def _check_config_domain(self) -> Optional[HttpResponse]:
|
||||||
|
"""Checks if current request's domain matches configured Domain, and
|
||||||
|
adds a warning if not."""
|
||||||
|
current_domain = self.request.get_host()
|
||||||
|
if ":" in current_domain:
|
||||||
|
current_domain, _ = current_domain.split(":")
|
||||||
|
config_domain = CONFIG.y("domain")
|
||||||
|
if current_domain != config_domain:
|
||||||
|
message = (
|
||||||
|
f"Current domain of '{current_domain}' doesn't "
|
||||||
|
f"match configured domain of '{config_domain}'."
|
||||||
|
)
|
||||||
|
LOGGER.warning(message)
|
||||||
|
return bad_request_message(self.request, message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_flow_non_applicable(self) -> HttpResponse:
|
||||||
|
"""When a flow is non-applicable check if user is on the correct domain"""
|
||||||
|
if NEXT_ARG_NAME in self.request.GET:
|
||||||
|
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||||
|
incorrect_domain_message = self._check_config_domain()
|
||||||
|
if incorrect_domain_message:
|
||||||
|
return incorrect_domain_message
|
||||||
|
# TODO: Add message
|
||||||
|
return redirect("passbook_core:index")
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||||
# Early check if theres an active Plan for the current session
|
# Early check if theres an active Plan for the current session
|
||||||
if SESSION_KEY_PLAN not in self.request.session:
|
if SESSION_KEY_PLAN not in self.request.session:
|
||||||
|
@ -250,7 +72,7 @@ class FlowExecutorView(View):
|
||||||
self.plan = self._initiate_plan()
|
self.plan = self._initiate_plan()
|
||||||
except FlowNonApplicableError as exc:
|
except FlowNonApplicableError as exc:
|
||||||
LOGGER.warning("Flow not applicable to current user", exc=exc)
|
LOGGER.warning("Flow not applicable to current user", exc=exc)
|
||||||
return redirect("passbook_core:index")
|
return self.handle_flow_non_applicable()
|
||||||
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]
|
||||||
|
@ -260,7 +82,6 @@ class FlowExecutorView(View):
|
||||||
LOGGER.debug("Current factor", current_factor=self.current_factor)
|
LOGGER.debug("Current factor", current_factor=self.current_factor)
|
||||||
factor_cls = path_to_class(self.current_factor.type)
|
factor_cls = path_to_class(self.current_factor.type)
|
||||||
self.current_factor_view = factor_cls(self)
|
self.current_factor_view = factor_cls(self)
|
||||||
# self.current_factor_view.pending_user = self.pending_user
|
|
||||||
self.current_factor_view.request = request
|
self.current_factor_view.request = request
|
||||||
return super().dispatch(request)
|
return super().dispatch(request)
|
||||||
|
|
||||||
|
@ -284,3 +105,48 @@ class FlowExecutorView(View):
|
||||||
plan = planner.plan(self.request)
|
plan = planner.plan(self.request)
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
def _flow_done(self) -> HttpResponse:
|
||||||
|
"""User Successfully passed all factors"""
|
||||||
|
backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend
|
||||||
|
login(
|
||||||
|
self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend
|
||||||
|
)
|
||||||
|
LOGGER.debug("Logged in", user=self.plan.context[PLAN_CONTEXT_PENDING_USER])
|
||||||
|
self.cancel()
|
||||||
|
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
|
||||||
|
if next_param and not is_url_absolute(next_param):
|
||||||
|
return redirect(next_param)
|
||||||
|
return redirect_with_qs("passbook_core:overview")
|
||||||
|
|
||||||
|
def factor_ok(self) -> HttpResponse:
|
||||||
|
"""Callback called by factors upon successful completion.
|
||||||
|
Persists updated plan and context to session."""
|
||||||
|
LOGGER.debug(
|
||||||
|
"Factor ok", factor_class=class_to_path(self.current_factor_view.__class__),
|
||||||
|
)
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||||
|
if self.plan.factors:
|
||||||
|
LOGGER.debug(
|
||||||
|
"Continuing with next factor", reamining=len(self.plan.factors)
|
||||||
|
)
|
||||||
|
return redirect_with_qs(
|
||||||
|
"passbook_flows:flow-executor", self.request.GET, **self.kwargs
|
||||||
|
)
|
||||||
|
# User passed all factors
|
||||||
|
LOGGER.debug(
|
||||||
|
"User passed all factors", user=self.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
)
|
||||||
|
return self._flow_done()
|
||||||
|
|
||||||
|
def factor_invalid(self) -> HttpResponse:
|
||||||
|
"""Callback used factor when data is correct but a policy denies access
|
||||||
|
or the user account is disabled."""
|
||||||
|
LOGGER.debug("User invalid")
|
||||||
|
self.cancel()
|
||||||
|
return redirect_with_qs("passbook_flows:auth-denied", self.request.GET)
|
||||||
|
|
||||||
|
def cancel(self) -> HttpResponse:
|
||||||
|
"""Cancel current execution and return a redirect"""
|
||||||
|
del self.request.session[SESSION_KEY_PLAN]
|
||||||
|
return redirect_with_qs("passbook_flows:auth-denied", self.request.GET)
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
"""URL-related utils"""
|
"""URL-related utils"""
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect, reverse
|
||||||
|
from django.utils.http import urlencode
|
||||||
|
|
||||||
|
|
||||||
def is_url_absolute(url):
|
def is_url_absolute(url):
|
||||||
"""Check if domain is absolute to prevent user from being redirect somewhere else"""
|
"""Check if domain is absolute to prevent user from being redirect somewhere else"""
|
||||||
return bool(urlparse(url).netloc)
|
return bool(urlparse(url).netloc)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_with_qs(view: str, get_query_set=None, **kwargs) -> HttpResponse:
|
||||||
|
"""Wrapper to redirect whilst keeping GET Parameters"""
|
||||||
|
target = reverse(view, kwargs=kwargs)
|
||||||
|
if get_query_set:
|
||||||
|
target += "?" + urlencode(get_query_set.items())
|
||||||
|
return redirect(target)
|
||||||
|
|
|
@ -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.flows.views import AuthenticationView
|
from passbook.flows.planner import PLAN_CONTEXT_SSO
|
||||||
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
|
||||||
|
|
||||||
|
@ -54,8 +54,9 @@ class Evaluator:
|
||||||
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
|
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
|
||||||
kwargs["pb_logger"] = get_logger()
|
kwargs["pb_logger"] = get_logger()
|
||||||
if request.http_request:
|
if request.http_request:
|
||||||
|
# TODO: Get access to current plan
|
||||||
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
|
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
|
||||||
AuthenticationView.SESSION_IS_SSO_LOGIN, False
|
PLAN_CONTEXT_SSO, False
|
||||||
)
|
)
|
||||||
kwargs["pb_client_ip"] = (
|
kwargs["pb_client_ip"] = (
|
||||||
get_client_ip(request.http_request) or "255.255.255.255"
|
get_client_ip(request.http_request) or "255.255.255.255"
|
||||||
|
|
|
@ -13,7 +13,15 @@ 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.flows.views import AuthenticationView, _redirect_with_qs
|
from passbook.factors.password.factor import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
|
from passbook.flows.models import Flow, FlowDesignation
|
||||||
|
from passbook.flows.planner import (
|
||||||
|
PLAN_CONTEXT_PENDING_USER,
|
||||||
|
PLAN_CONTEXT_SSO,
|
||||||
|
FlowPlanner,
|
||||||
|
)
|
||||||
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.lib.utils.urls import redirect_with_qs
|
||||||
from passbook.sources.oauth.clients import get_client
|
from passbook.sources.oauth.clients import get_client
|
||||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
|
|
||||||
|
@ -165,10 +173,17 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
source=access.source, identifier=access.identifier, request=self.request
|
source=access.source, identifier=access.identifier, request=self.request
|
||||||
)
|
)
|
||||||
self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk
|
# We run the Flow planner here so we can pass the Pending user in the context
|
||||||
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
|
flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
|
||||||
self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True
|
planner = FlowPlanner(flow)
|
||||||
return _redirect_with_qs("passbook_flows:auth-process", self.request.GET)
|
plan = planner.plan(self.request)
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||||
|
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = user.backend
|
||||||
|
plan.context[PLAN_CONTEXT_SSO] = True
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"passbook_flows:flow-executor", self.request.GET, flow_slug=flow.slug,
|
||||||
|
)
|
||||||
|
|
||||||
# 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):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
isort -rc passbook
|
isort -rc passbook
|
||||||
pyright
|
pyright
|
||||||
black passbook
|
black passbook
|
||||||
scripts/coverage.sh
|
# scripts/coverage.sh
|
||||||
bandit -r passbook
|
bandit -r passbook
|
||||||
pylint passbook
|
pylint passbook
|
||||||
prospector
|
prospector
|
||||||
|
|
Reference in New Issue