flows: add ConfigurableStage base class and ConfigureFlowInitView
This commit is contained in:
18
passbook/flows/migrations/0013_auto_20200924_1605.py
Normal file
18
passbook/flows/migrations/0013_auto_20200924_1605.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-24 16:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_flows', '0012_auto_20200908_1542'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='flow',
|
||||
name='designation',
|
||||
field=models.CharField(choices=[('authentication', 'Authentication'), ('authorization', 'Authorization'), ('invalidation', 'Invalidation'), ('enrollment', 'Enrollment'), ('unenrollment', 'Unrenollment'), ('recovery', 'Recovery'), ('stage_configuration', 'Stage Configuration')], max_length=100),
|
||||
),
|
||||
]
|
||||
@ -37,7 +37,7 @@ class FlowDesignation(models.TextChoices):
|
||||
ENROLLMENT = "enrollment"
|
||||
UNRENOLLMENT = "unenrollment"
|
||||
RECOVERY = "recovery"
|
||||
STAGE_SETUP = "stage_setup"
|
||||
STAGE_CONFIGURATION = "stage_configuration"
|
||||
|
||||
|
||||
class Stage(SerializerModel):
|
||||
@ -73,6 +73,29 @@ class Stage(SerializerModel):
|
||||
return f"Stage {self.name}"
|
||||
|
||||
|
||||
class ConfigurableStage(models.Model):
|
||||
"""Abstract base class for a Stage that can be configured by the enduser.
|
||||
The stage should create a default flow with the configure_stage designation during
|
||||
migration."""
|
||||
|
||||
configure_flow = models.ForeignKey(
|
||||
'passbook_flows.Flow',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
(
|
||||
"Flow used by an authenticated user to change their password. "
|
||||
"If empty, user will be unable to change their password."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
|
||||
|
||||
def in_memory_stage(view: Type["StageView"]) -> Stage:
|
||||
"""Creates an in-memory stage instance, based on a `_type` as view."""
|
||||
stage = Stage()
|
||||
|
||||
@ -3,7 +3,7 @@ from django.urls import path
|
||||
|
||||
from passbook.flows.models import FlowDesignation
|
||||
from passbook.flows.views import (
|
||||
CancelView,
|
||||
CancelView, ConfigureFlowInitView,
|
||||
FlowExecutorShellView,
|
||||
FlowExecutorView,
|
||||
ToDefaultFlow,
|
||||
@ -36,6 +36,7 @@ urlpatterns = [
|
||||
name="default-unenrollment",
|
||||
),
|
||||
path("-/cancel/", CancelView.as_view(), name="cancel"),
|
||||
path("-/configure/<uuid:stage_uuid>/", ConfigureFlowInitView.as_view(), name="configure"),
|
||||
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||
path(
|
||||
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""passbook multi-stage authentication engine"""
|
||||
from traceback import format_tb
|
||||
from typing import Any, Dict, Optional
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
from django.http import (
|
||||
Http404,
|
||||
@ -19,8 +20,8 @@ from structlog import get_logger
|
||||
from passbook.audit.models import cleanse_dict
|
||||
from passbook.core.models import PASSBOOK_USER_DEBUG
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.models import Flow, FlowDesignation, Stage
|
||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
||||
from passbook.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
||||
from passbook.flows.planner import FlowPlan, FlowPlanner, PLAN_CONTEXT_PENDING_USER
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||
from passbook.policies.http import AccessDeniedResponse
|
||||
@ -295,3 +296,32 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
||||
{"type": "template", "body": source.content.decode("utf-8")}
|
||||
)
|
||||
return source
|
||||
|
||||
|
||||
class ConfigureFlowInitView(LoginRequiredMixin, View):
|
||||
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||
or raise Http404 if no configure_flow has been set."""
|
||||
|
||||
def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse:
|
||||
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||
or raise Http404 if no configure_flow has been set."""
|
||||
try:
|
||||
stage: Stage = Stage.objects.get_subclass(pk=stage_uuid)
|
||||
except Stage.DoesNotExist as exc:
|
||||
raise Http404 from exc
|
||||
if not issubclass(stage, ConfigurableStage):
|
||||
LOGGER.debug("Stage does not inherit ConfigurableStage", stage=stage)
|
||||
raise Http404
|
||||
if not stage.configure_flow:
|
||||
LOGGER.debug("Stage has no configure_flow set", stage=stage)
|
||||
raise Http404
|
||||
|
||||
plan = FlowPlanner(stage.configure_flow).plan(
|
||||
request, {PLAN_CONTEXT_PENDING_USER: request.user}
|
||||
)
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor-shell",
|
||||
self.request.GET,
|
||||
flow_slug=stage.configure_flow.slug,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user