flows: separate final login step from flow executor
This commit is contained in:
parent
0aad0604d8
commit
c46f0781fc
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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(),
|
||||
}
|
|
@ -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",),
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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()
|
Reference in New Issue