flows: separate final login step from flow executor

This commit is contained in:
Jens Langhammer 2020-05-09 23:19:36 +02:00
parent 0aad0604d8
commit c46f0781fc
11 changed files with 166 additions and 13 deletions

View file

@ -33,6 +33,8 @@ 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.identification.api import IdentificationStageViewSet
from passbook.stages.login.api import LoginStageViewSet
from passbook.stages.otp.api import OTPStageViewSet
from passbook.stages.password.api import PasswordStageViewSet
@ -49,10 +51,13 @@ router.register("core/applications", ApplicationViewSet)
router.register("core/invitations", InvitationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)
router.register("audit/events", EventViewSet)
router.register("sources/all", SourceViewSet)
router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
@ -60,20 +65,26 @@ router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet)
router.register("policies/webhook", WebhookPolicyViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("providers/all", ProviderViewSet)
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
router.register("providers/oauth", OAuth2ProviderViewSet)
router.register("providers/openid", OpenIDProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet)
router.register("propertymappings/all", PropertyMappingViewSet)
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("stages/all", StageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/dummy", DummyStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/otp", OTPStageViewSet)
router.register("stages/password", PasswordStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/login", LoginStageViewSet)
router.register("flows", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet)

View file

@ -12,6 +12,9 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("passbook_flows", "Flow")
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
LoginStage = apps.get_model(
"passbook_stages_login", "LoginStage"
)
IdentificationStage = apps.get_model(
"passbook_stages_identification", "IdentificationStage"
)
@ -33,8 +36,12 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
name="password", backends=["django.contrib.auth.backends.ModelBackend"],
)
if not LoginStage.objects.using(db_alias).exists():
LoginStage.objects.using(db_alias).create(name="authentication")
ident_stage = IdentificationStage.objects.using(db_alias).first()
pw_stage = PasswordStage.objects.using(db_alias).first()
login_stage = LoginStage.objects.using(db_alias).first()
flow = Flow.objects.using(db_alias).create(
name="default-authentication-flow",
slug="default-authentication-flow",
@ -46,12 +53,16 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
FlowStageBinding.objects.using(db_alias).create(
flow=flow, stage=pw_stage, order=1,
)
FlowStageBinding.objects.using(db_alias).create(
flow=flow, stage=login_stage, order=2,
)
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0001_initial"),
("passbook_stages_login", "0001_initial"),
("passbook_stages_password", "0001_initial"),
("passbook_stages_identification", "0001_initial"),
]

View file

@ -1,7 +1,6 @@
"""passbook multi-stage authentication engine"""
from typing import Optional
from django.contrib.auth import login
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import View
@ -27,7 +26,7 @@ class FlowExecutorView(View):
flow: Flow
plan: FlowPlan
plan: Optional[FlowPlan] = None
current_stage: Stage
current_stage_view: View
@ -116,15 +115,6 @@ class FlowExecutorView(View):
def _flow_done(self) -> HttpResponse:
"""User Successfully passed all stages"""
backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend
login(
self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend
)
LOGGER.debug(
"Logged in",
user=self.plan.context[PLAN_CONTEXT_PENDING_USER],
flow_slug=self.flow.slug,
)
self.cancel()
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
if next_param and not is_url_absolute(next_param):
@ -165,10 +155,9 @@ class FlowExecutorView(View):
self.cancel()
return redirect_with_qs("passbook_flows:denied", self.request.GET)
def cancel(self) -> HttpResponse:
def cancel(self):
"""Cancel current execution and return a redirect"""
del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs("passbook_flows:denied", self.request.GET)
class FlowPermissionDeniedView(PermissionDeniedView):

View file

View file

@ -0,0 +1,24 @@
"""Login Stage API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.stages.login.models import LoginStage
class LoginStageSerializer(ModelSerializer):
"""LoginStage Serializer"""
class Meta:
model = LoginStage
fields = [
"pk",
"name",
]
class LoginStageViewSet(ModelViewSet):
"""LoginStage Viewset"""
queryset = LoginStage.objects.all()
serializer_class = LoginStageSerializer

View file

@ -0,0 +1,10 @@
"""passbook login stage app config"""
from django.apps import AppConfig
class PassbookStageLoginConfig(AppConfig):
"""passbook login stage config"""
name = "passbook.stages.login"
label = "passbook_stages_login"
verbose_name = "passbook Stages.Login"

View file

@ -0,0 +1,16 @@
"""passbook flows login forms"""
from django import forms
from passbook.stages.login.models import LoginStage
class LoginStageForm(forms.ModelForm):
"""Form to create/edit LoginStage instances"""
class Meta:
model = LoginStage
fields = ["name"]
widgets = {
"name": forms.TextInput(),
}

View file

@ -0,0 +1,37 @@
# Generated by Django 3.0.3 on 2020-05-09 20:26
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="LoginStage",
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",
),
),
],
options={
"verbose_name": "Login Stage",
"verbose_name_plural": "Login Stages",
},
bases=("passbook_flows.stage",),
),
]

View file

@ -0,0 +1,19 @@
"""login stage models"""
from django.utils.translation import gettext_lazy as _
from passbook.flows.models import Stage
class LoginStage(Stage):
"""Login stage, allows a user to identify themselves to authenticate."""
type = "passbook.stages.login.stage.LoginStageView"
form = "passbook.stages.login.forms.LoginStageForm"
def __str__(self):
return f"Login Stage {self.name}"
class Meta:
verbose_name = _("Login Stage")
verbose_name_plural = _("Login Stages")

View file

@ -0,0 +1,36 @@
"""Login stage logic"""
from django.contrib import messages
from django.contrib.auth import login
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import AuthenticationStage
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
LOGGER = get_logger()
class LoginStageView(AuthenticationStage):
"""Finalise Authentication flow by logging the user in"""
def get(self, request: HttpRequest) -> HttpResponse:
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
messages.error(request, _("No Pending user to login."))
return self.executor.stage_invalid()
if PLAN_CONTEXT_AUTHENTICATION_BACKEND not in self.executor.plan.context:
messages.error(request, _("Pending user has no backend."))
return self.executor.stage_invalid()
backend = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].backend
login(
self.request,
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
backend=backend,
)
LOGGER.debug(
"Logged in",
user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
flow_slug=self.executor.flow.slug,
)
return self.executor.stage_ok()