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.captcha.api import CaptchaStageViewSet
|
||||||
from passbook.stages.dummy.api import DummyStageViewSet
|
from passbook.stages.dummy.api import DummyStageViewSet
|
||||||
from passbook.stages.email.api import EmailStageViewSet
|
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.otp.api import OTPStageViewSet
|
||||||
from passbook.stages.password.api import PasswordStageViewSet
|
from passbook.stages.password.api import PasswordStageViewSet
|
||||||
|
|
||||||
|
@ -49,10 +51,13 @@ router.register("core/applications", ApplicationViewSet)
|
||||||
router.register("core/invitations", InvitationViewSet)
|
router.register("core/invitations", InvitationViewSet)
|
||||||
router.register("core/groups", GroupViewSet)
|
router.register("core/groups", GroupViewSet)
|
||||||
router.register("core/users", UserViewSet)
|
router.register("core/users", UserViewSet)
|
||||||
|
|
||||||
router.register("audit/events", EventViewSet)
|
router.register("audit/events", EventViewSet)
|
||||||
|
|
||||||
router.register("sources/all", SourceViewSet)
|
router.register("sources/all", SourceViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
router.register("sources/oauth", OAuthSourceViewSet)
|
router.register("sources/oauth", OAuthSourceViewSet)
|
||||||
|
|
||||||
router.register("policies/all", PolicyViewSet)
|
router.register("policies/all", PolicyViewSet)
|
||||||
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
|
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
|
||||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||||
|
@ -60,20 +65,26 @@ router.register("policies/password", PasswordPolicyViewSet)
|
||||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||||
router.register("policies/webhook", WebhookPolicyViewSet)
|
router.register("policies/webhook", WebhookPolicyViewSet)
|
||||||
router.register("policies/expression", ExpressionPolicyViewSet)
|
router.register("policies/expression", ExpressionPolicyViewSet)
|
||||||
|
|
||||||
router.register("providers/all", ProviderViewSet)
|
router.register("providers/all", ProviderViewSet)
|
||||||
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
|
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
|
||||||
router.register("providers/oauth", OAuth2ProviderViewSet)
|
router.register("providers/oauth", OAuth2ProviderViewSet)
|
||||||
router.register("providers/openid", OpenIDProviderViewSet)
|
router.register("providers/openid", OpenIDProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
|
|
||||||
router.register("propertymappings/all", PropertyMappingViewSet)
|
router.register("propertymappings/all", PropertyMappingViewSet)
|
||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
|
|
||||||
router.register("stages/all", StageViewSet)
|
router.register("stages/all", StageViewSet)
|
||||||
router.register("stages/captcha", CaptchaStageViewSet)
|
router.register("stages/captcha", CaptchaStageViewSet)
|
||||||
router.register("stages/dummy", DummyStageViewSet)
|
router.register("stages/dummy", DummyStageViewSet)
|
||||||
router.register("stages/email", EmailStageViewSet)
|
router.register("stages/email", EmailStageViewSet)
|
||||||
router.register("stages/otp", OTPStageViewSet)
|
router.register("stages/otp", OTPStageViewSet)
|
||||||
router.register("stages/password", PasswordStageViewSet)
|
router.register("stages/password", PasswordStageViewSet)
|
||||||
|
router.register("stages/identification", IdentificationStageViewSet)
|
||||||
|
router.register("stages/login", LoginStageViewSet)
|
||||||
|
|
||||||
router.register("flows", FlowViewSet)
|
router.register("flows", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
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")
|
Flow = apps.get_model("passbook_flows", "Flow")
|
||||||
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||||
PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
|
PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
|
||||||
|
LoginStage = apps.get_model(
|
||||||
|
"passbook_stages_login", "LoginStage"
|
||||||
|
)
|
||||||
IdentificationStage = apps.get_model(
|
IdentificationStage = apps.get_model(
|
||||||
"passbook_stages_identification", "IdentificationStage"
|
"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"],
|
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()
|
ident_stage = IdentificationStage.objects.using(db_alias).first()
|
||||||
pw_stage = PasswordStage.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(
|
flow = Flow.objects.using(db_alias).create(
|
||||||
name="default-authentication-flow",
|
name="default-authentication-flow",
|
||||||
slug="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(
|
FlowStageBinding.objects.using(db_alias).create(
|
||||||
flow=flow, stage=pw_stage, order=1,
|
flow=flow, stage=pw_stage, order=1,
|
||||||
)
|
)
|
||||||
|
FlowStageBinding.objects.using(db_alias).create(
|
||||||
|
flow=flow, stage=login_stage, order=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_flows", "0001_initial"),
|
("passbook_flows", "0001_initial"),
|
||||||
|
("passbook_stages_login", "0001_initial"),
|
||||||
("passbook_stages_password", "0001_initial"),
|
("passbook_stages_password", "0001_initial"),
|
||||||
("passbook_stages_identification", "0001_initial"),
|
("passbook_stages_identification", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""passbook multi-stage authentication engine"""
|
"""passbook multi-stage authentication engine"""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
@ -27,7 +26,7 @@ class FlowExecutorView(View):
|
||||||
|
|
||||||
flow: Flow
|
flow: Flow
|
||||||
|
|
||||||
plan: FlowPlan
|
plan: Optional[FlowPlan] = None
|
||||||
current_stage: Stage
|
current_stage: Stage
|
||||||
current_stage_view: View
|
current_stage_view: View
|
||||||
|
|
||||||
|
@ -116,15 +115,6 @@ class FlowExecutorView(View):
|
||||||
|
|
||||||
def _flow_done(self) -> HttpResponse:
|
def _flow_done(self) -> HttpResponse:
|
||||||
"""User Successfully passed all stages"""
|
"""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()
|
self.cancel()
|
||||||
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
|
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
|
||||||
if next_param and not is_url_absolute(next_param):
|
if next_param and not is_url_absolute(next_param):
|
||||||
|
@ -165,10 +155,9 @@ class FlowExecutorView(View):
|
||||||
self.cancel()
|
self.cancel()
|
||||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
||||||
|
|
||||||
def cancel(self) -> HttpResponse:
|
def cancel(self):
|
||||||
"""Cancel current execution and return a redirect"""
|
"""Cancel current execution and return a redirect"""
|
||||||
del self.request.session[SESSION_KEY_PLAN]
|
del self.request.session[SESSION_KEY_PLAN]
|
||||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowPermissionDeniedView(PermissionDeniedView):
|
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