stages/user_create: add stage to create user after prompts
This commit is contained in:
		| @ -37,6 +37,7 @@ from passbook.stages.identification.api import IdentificationStageViewSet | |||||||
| 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 | ||||||
| from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet | from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet | ||||||
|  | from passbook.stages.user_create.api import UserCreateStageViewSet | ||||||
| from passbook.stages.user_login.api import UserLoginStageViewSet | from passbook.stages.user_login.api import UserLoginStageViewSet | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -85,6 +86,7 @@ router.register("stages/otp", OTPStageViewSet) | |||||||
| router.register("stages/password", PasswordStageViewSet) | router.register("stages/password", PasswordStageViewSet) | ||||||
| router.register("stages/prompt", PromptStageViewSet) | router.register("stages/prompt", PromptStageViewSet) | ||||||
| router.register("stages/prompt/prompts", PromptViewSet) | router.register("stages/prompt/prompts", PromptViewSet) | ||||||
|  | router.register("stages/user_create", UserCreateStageViewSet) | ||||||
| router.register("stages/user_login", UserLoginStageViewSet) | router.register("stages/user_login", UserLoginStageViewSet) | ||||||
|  |  | ||||||
| router.register("flows", FlowViewSet) | router.register("flows", FlowViewSet) | ||||||
|  | |||||||
| @ -107,6 +107,7 @@ INSTALLED_APPS = [ | |||||||
|     "passbook.stages.email.apps.PassbookStageEmailConfig", |     "passbook.stages.email.apps.PassbookStageEmailConfig", | ||||||
|     "passbook.stages.prompt.apps.PassbookStagPromptConfig", |     "passbook.stages.prompt.apps.PassbookStagPromptConfig", | ||||||
|     "passbook.stages.identification.apps.PassbookStageIdentificationConfig", |     "passbook.stages.identification.apps.PassbookStageIdentificationConfig", | ||||||
|  |     "passbook.stages.user_create.apps.PassbookStageUserCreateConfig", | ||||||
|     "passbook.stages.user_login.apps.PassbookStageUserLoginConfig", |     "passbook.stages.user_login.apps.PassbookStageUserLoginConfig", | ||||||
|     "passbook.stages.otp.apps.PassbookStageOTPConfig", |     "passbook.stages.otp.apps.PassbookStageOTPConfig", | ||||||
|     "passbook.stages.password.apps.PassbookStagePasswordConfig", |     "passbook.stages.password.apps.PassbookStagePasswordConfig", | ||||||
| @ -357,7 +358,7 @@ TEST_OUTPUT_VERBOSE = 2 | |||||||
| TEST_OUTPUT_FILE_NAME = "unittest.xml" | TEST_OUTPUT_FILE_NAME = "unittest.xml" | ||||||
|  |  | ||||||
| if any("test" in arg for arg in sys.argv): | if any("test" in arg for arg in sys.argv): | ||||||
|     LOGGING = None |     # LOGGING = None | ||||||
|     TEST = True |     TEST = True | ||||||
|     CELERY_TASK_ALWAYS_EAGER = True |     CELERY_TASK_ALWAYS_EAGER = True | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								passbook/stages/user_create/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/user_create/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								passbook/stages/user_create/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/stages/user_create/api.py
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										10
									
								
								passbook/stages/user_create/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/stages/user_create/apps.py
									
									
									
									
									
										Normal 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" | ||||||
							
								
								
									
										16
									
								
								passbook/stages/user_create/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/stages/user_create/forms.py
									
									
									
									
									
										Normal 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(), | ||||||
|  |         } | ||||||
							
								
								
									
										37
									
								
								passbook/stages/user_create/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								passbook/stages/user_create/migrations/0001_initial.py
									
									
									
									
									
										Normal 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",), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								passbook/stages/user_create/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/user_create/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								passbook/stages/user_create/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								passbook/stages/user_create/models.py
									
									
									
									
									
										Normal 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") | ||||||
							
								
								
									
										39
									
								
								passbook/stages/user_create/stage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								passbook/stages/user_create/stage.py
									
									
									
									
									
										Normal 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() | ||||||
							
								
								
									
										73
									
								
								passbook/stages/user_create/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								passbook/stages/user_create/tests.py
									
									
									
									
									
										Normal 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")) | ||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer