stages/user_create: add stage to create user after prompts

This commit is contained in:
Jens Langhammer 2020-05-10 18:04:23 +02:00
parent f6461b08d7
commit 8dc3c49a2f
11 changed files with 222 additions and 1 deletions

View file

@ -37,6 +37,7 @@ from passbook.stages.identification.api import IdentificationStageViewSet
from passbook.stages.otp.api import OTPStageViewSet
from passbook.stages.password.api import PasswordStageViewSet
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
from passbook.stages.user_create.api import UserCreateStageViewSet
from passbook.stages.user_login.api import UserLoginStageViewSet
LOGGER = get_logger()
@ -85,6 +86,7 @@ router.register("stages/otp", OTPStageViewSet)
router.register("stages/password", PasswordStageViewSet)
router.register("stages/prompt", PromptStageViewSet)
router.register("stages/prompt/prompts", PromptViewSet)
router.register("stages/user_create", UserCreateStageViewSet)
router.register("stages/user_login", UserLoginStageViewSet)
router.register("flows", FlowViewSet)

View file

@ -107,6 +107,7 @@ INSTALLED_APPS = [
"passbook.stages.email.apps.PassbookStageEmailConfig",
"passbook.stages.prompt.apps.PassbookStagPromptConfig",
"passbook.stages.identification.apps.PassbookStageIdentificationConfig",
"passbook.stages.user_create.apps.PassbookStageUserCreateConfig",
"passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
"passbook.stages.otp.apps.PassbookStageOTPConfig",
"passbook.stages.password.apps.PassbookStagePasswordConfig",
@ -357,7 +358,7 @@ TEST_OUTPUT_VERBOSE = 2
TEST_OUTPUT_FILE_NAME = "unittest.xml"
if any("test" in arg for arg in sys.argv):
LOGGING = None
# LOGGING = None
TEST = True
CELERY_TASK_ALWAYS_EAGER = True

View file

View file

@ -0,0 +1,24 @@
"""User Create Stage API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.stages.user_create.models import UserCreateStage
class UserCreateStageSerializer(ModelSerializer):
"""UserCreateStage Serializer"""
class Meta:
model = UserCreateStage
fields = [
"pk",
"name",
]
class UserCreateStageViewSet(ModelViewSet):
"""UserCreateStage Viewset"""
queryset = UserCreateStage.objects.all()
serializer_class = UserCreateStageSerializer

View file

@ -0,0 +1,10 @@
"""passbook create stage app config"""
from django.apps import AppConfig
class PassbookStageUserCreateConfig(AppConfig):
"""passbook create stage config"""
name = "passbook.stages.user_create"
label = "passbook_stages_user_create"
verbose_name = "passbook Stages.User Create"

View file

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

View file

@ -0,0 +1,37 @@
# Generated by Django 3.0.5 on 2020-05-10 14:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_flows", "0003_auto_20200509_1258"),
]
operations = [
migrations.CreateModel(
name="UserCreateStage",
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": "User Create Stage",
"verbose_name_plural": "User Create Stages",
},
bases=("passbook_flows.stage",),
),
]

View file

@ -0,0 +1,19 @@
"""create stage models"""
from django.utils.translation import gettext_lazy as _
from passbook.flows.models import Stage
class UserCreateStage(Stage):
"""Create stage, create a user from saved data."""
type = "passbook.stages.user_create.stage.UserCreateStageView"
form = "passbook.stages.user_create.forms.UserCreateStageForm"
def __str__(self):
return f"User Create Stage {self.name}"
class Meta:
verbose_name = _("User Create Stage")
verbose_name_plural = _("User Create Stages")

View file

@ -0,0 +1,39 @@
"""Create stage logic"""
from django.contrib import messages
from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import User
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import AuthenticationStage
from passbook.lib.utils.reflection import class_to_path
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger()
class UserCreateStageView(AuthenticationStage):
"""Finalise Enrollment flow by creating a user object."""
def get(self, request: HttpRequest) -> HttpResponse:
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
message = _("No Pending data.")
messages.error(request, message)
LOGGER.debug(message)
return self.executor.stage_invalid()
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
user = User.objects.create_user(**data)
# Set created user as pending_user, so this can be chained with user_login
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = user
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = class_to_path(
ModelBackend
)
LOGGER.debug(
"Created user",
user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
flow_slug=self.executor.flow.slug,
)
return self.executor.stage_ok()

View file

@ -0,0 +1,73 @@
"""create tests"""
import string
from random import SystemRandom
from django.shortcuts import reverse
from django.test import Client, TestCase
from passbook.core.models import User
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from passbook.stages.user_create.models import UserCreateStage
class TestUserCreateStage(TestCase):
"""Create tests"""
def setUp(self):
super().setUp()
self.client = Client()
self.password = "".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
self.flow = Flow.objects.create(
name="test-create",
slug="test-create",
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = UserCreateStage.objects.create(name="create")
FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
def test_valid_create(self):
"""Test creation of user"""
plan = FlowPlan(stages=[self.stage])
plan.context[PLAN_CONTEXT_PROMPT] = {
"username": "test-user",
"name": "name",
"email": "test@beryju.org",
"password": self.password,
}
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
)
self.assertEqual(response.status_code, 302)
self.assertTrue(
User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
).exists()
)
def test_without_data(self):
"""Test without data results in error"""
plan = FlowPlan(stages=[self.stage])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("passbook_flows:denied"))