stages/invitation: start extracting invitation from core
This commit is contained in:
		@ -11,17 +11,17 @@ from django.utils.translation import ugettext as _
 | 
			
		||||
from django.views.generic import DeleteView, ListView
 | 
			
		||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
 | 
			
		||||
from passbook.core.forms.invitations import InvitationForm
 | 
			
		||||
from passbook.core.models import Invitation
 | 
			
		||||
from passbook.core.signals import invitation_created
 | 
			
		||||
from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
from passbook.stages.invitation.forms import InvitationForm
 | 
			
		||||
from passbook.stages.invitation.models import Invitation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
    """Show list of all invitations"""
 | 
			
		||||
 | 
			
		||||
    model = Invitation
 | 
			
		||||
    permission_required = "passbook_core.view_invitation"
 | 
			
		||||
    permission_required = "passbook_stages_invitation.view_invitation"
 | 
			
		||||
    template_name = "administration/invitation/list.html"
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
    ordering = "-expires"
 | 
			
		||||
@ -37,7 +37,7 @@ class InvitationCreateView(
 | 
			
		||||
 | 
			
		||||
    model = Invitation
 | 
			
		||||
    form_class = InvitationForm
 | 
			
		||||
    permission_required = "passbook_core.add_invitation"
 | 
			
		||||
    permission_required = "passbook_stages_invitation.add_invitation"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/create.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:invitations")
 | 
			
		||||
@ -61,7 +61,7 @@ class InvitationDeleteView(
 | 
			
		||||
    """Delete invitation"""
 | 
			
		||||
 | 
			
		||||
    model = Invitation
 | 
			
		||||
    permission_required = "passbook_core.delete_invitation"
 | 
			
		||||
    permission_required = "passbook_stages_invitation.delete_invitation"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/delete.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:invitations")
 | 
			
		||||
 | 
			
		||||
@ -5,9 +5,10 @@ from django.views.generic import TemplateView
 | 
			
		||||
 | 
			
		||||
from passbook import __version__
 | 
			
		||||
from passbook.admin.mixins import AdminRequiredMixin
 | 
			
		||||
from passbook.core.models import Application, Invitation, Policy, Provider, Source, User
 | 
			
		||||
from passbook.core.models import Application, Policy, Provider, Source, User
 | 
			
		||||
from passbook.flows.models import Flow, Stage
 | 
			
		||||
from passbook.root.celery import CELERY_APP
 | 
			
		||||
from passbook.stages.invitation.models import Invitation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,6 @@ from passbook.api.permissions import CustomObjectPermissions
 | 
			
		||||
from passbook.audit.api import EventViewSet
 | 
			
		||||
from passbook.core.api.applications import ApplicationViewSet
 | 
			
		||||
from passbook.core.api.groups import GroupViewSet
 | 
			
		||||
from passbook.core.api.invitations import InvitationViewSet
 | 
			
		||||
from passbook.core.api.policies import PolicyViewSet
 | 
			
		||||
from passbook.core.api.propertymappings import PropertyMappingViewSet
 | 
			
		||||
from passbook.core.api.providers import ProviderViewSet
 | 
			
		||||
@ -34,6 +33,7 @@ from passbook.sources.oauth.api import OAuthSourceViewSet
 | 
			
		||||
from passbook.stages.captcha.api import CaptchaStageViewSet
 | 
			
		||||
from passbook.stages.email.api import EmailStageViewSet
 | 
			
		||||
from passbook.stages.identification.api import IdentificationStageViewSet
 | 
			
		||||
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
 | 
			
		||||
from passbook.stages.otp.api import OTPStageViewSet
 | 
			
		||||
from passbook.stages.password.api import PasswordStageViewSet
 | 
			
		||||
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
 | 
			
		||||
@ -51,7 +51,6 @@ for _passbook_app in get_apps():
 | 
			
		||||
        LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
 | 
			
		||||
 | 
			
		||||
router.register("core/applications", ApplicationViewSet)
 | 
			
		||||
router.register("core/invitations", InvitationViewSet)
 | 
			
		||||
router.register("core/groups", GroupViewSet)
 | 
			
		||||
router.register("core/users", UserViewSet)
 | 
			
		||||
 | 
			
		||||
@ -83,6 +82,8 @@ router.register("stages/all", StageViewSet)
 | 
			
		||||
router.register("stages/captcha", CaptchaStageViewSet)
 | 
			
		||||
router.register("stages/email", EmailStageViewSet)
 | 
			
		||||
router.register("stages/identification", IdentificationStageViewSet)
 | 
			
		||||
router.register("stages/invitation", InvitationStageViewSet)
 | 
			
		||||
router.register("stages/invitation/invitations", InvitationViewSet)
 | 
			
		||||
router.register("stages/otp", OTPStageViewSet)
 | 
			
		||||
router.register("stages/password", PasswordStageViewSet)
 | 
			
		||||
router.register("stages/prompt", PromptStageViewSet)
 | 
			
		||||
 | 
			
		||||
@ -1,38 +0,0 @@
 | 
			
		||||
"""passbook core invitation form"""
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Invitation, User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationForm(forms.ModelForm):
 | 
			
		||||
    """InvitationForm"""
 | 
			
		||||
 | 
			
		||||
    def clean_fixed_username(self):
 | 
			
		||||
        """Check if username is already used"""
 | 
			
		||||
        username = self.cleaned_data.get("fixed_username")
 | 
			
		||||
        if User.objects.filter(username=username).exists():
 | 
			
		||||
            raise ValidationError(_("Username is already in use."))
 | 
			
		||||
        return username
 | 
			
		||||
 | 
			
		||||
    def clean_fixed_email(self):
 | 
			
		||||
        """Check if email is already used"""
 | 
			
		||||
        email = self.cleaned_data.get("fixed_email")
 | 
			
		||||
        if User.objects.filter(email=email).exists():
 | 
			
		||||
            raise ValidationError(_("E-Mail is already in use."))
 | 
			
		||||
        return email
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = Invitation
 | 
			
		||||
        fields = ["expires", "fixed_username", "fixed_email", "needs_confirmation"]
 | 
			
		||||
        labels = {
 | 
			
		||||
            "fixed_username": "Force user's username (optional)",
 | 
			
		||||
            "fixed_email": "Force user's email (optional)",
 | 
			
		||||
        }
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "fixed_username": forms.TextInput(),
 | 
			
		||||
            "fixed_email": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										14
									
								
								passbook/core/migrations/0014_delete_invitation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								passbook/core/migrations/0014_delete_invitation.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
# Generated by Django 3.0.5 on 2020-05-11 19:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_core", "0013_delete_debugpolicy"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.DeleteModel(name="Invitation",),
 | 
			
		||||
    ]
 | 
			
		||||
@ -8,7 +8,6 @@ from django.contrib.postgres.fields import JSONField
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_prometheus.models import ExportModelOperationsMixin
 | 
			
		||||
@ -196,30 +195,6 @@ class Policy(ExportModelOperationsMixin("policy"), UUIDModel, CreatedUpdatedMode
 | 
			
		||||
        raise PolicyException()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Invitation(ExportModelOperationsMixin("invitation"), UUIDModel):
 | 
			
		||||
    """Single-use invitation link"""
 | 
			
		||||
 | 
			
		||||
    created_by = models.ForeignKey("User", on_delete=models.CASCADE)
 | 
			
		||||
    expires = models.DateTimeField(default=None, blank=True, null=True)
 | 
			
		||||
    fixed_username = models.TextField(blank=True, default=None)
 | 
			
		||||
    fixed_email = models.TextField(blank=True, default=None)
 | 
			
		||||
    needs_confirmation = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def link(self):
 | 
			
		||||
        """Get link to use invitation"""
 | 
			
		||||
        qs = f"?invitation={self.uuid.hex}"
 | 
			
		||||
        return reverse_lazy("passbook_flows:default-enrollment") + qs
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"Invitation {self.uuid.hex} created by {self.created_by}"
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Invitation")
 | 
			
		||||
        verbose_name_plural = _("Invitations")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
 | 
			
		||||
    """One-time link for password resets/sign-up-confirmations"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ def redirect_with_qs(view: str, get_query_set=None, **kwargs) -> HttpResponse:
 | 
			
		||||
    try:
 | 
			
		||||
        target = reverse(view, kwargs=kwargs)
 | 
			
		||||
    except NoReverseMatch:
 | 
			
		||||
        if not is_url_absolute(view):
 | 
			
		||||
            return redirect(view)
 | 
			
		||||
        LOGGER.debug("redirect target is not a valid view", view=view)
 | 
			
		||||
        raise
 | 
			
		||||
    else:
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    "passbook.stages.email.apps.PassbookStageEmailConfig",
 | 
			
		||||
    "passbook.stages.prompt.apps.PassbookStagPromptConfig",
 | 
			
		||||
    "passbook.stages.identification.apps.PassbookStageIdentificationConfig",
 | 
			
		||||
    "passbook.stages.invitation.apps.PassbookStageUserInvitationConfig",
 | 
			
		||||
    "passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
 | 
			
		||||
    "passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig",
 | 
			
		||||
    "passbook.stages.user_write.apps.PassbookStageUserWriteConfig",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/stages/invitation/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/invitation/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -1,8 +1,28 @@
 | 
			
		||||
"""Invitation API Views"""
 | 
			
		||||
"""Invitation Stage API Views"""
 | 
			
		||||
from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Invitation
 | 
			
		||||
from passbook.stages.invitation.models import Invitation, InvitationStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationStageSerializer(ModelSerializer):
 | 
			
		||||
    """InvitationStage Serializer"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = InvitationStage
 | 
			
		||||
        fields = [
 | 
			
		||||
            "pk",
 | 
			
		||||
            "name",
 | 
			
		||||
            "continue_flow_without_invitation",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationStageViewSet(ModelViewSet):
 | 
			
		||||
    """InvitationStage Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = InvitationStage.objects.all()
 | 
			
		||||
    serializer_class = InvitationStageSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationSerializer(ModelSerializer):
 | 
			
		||||
							
								
								
									
										10
									
								
								passbook/stages/invitation/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/stages/invitation/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
"""passbook invitation stage app config"""
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PassbookStageUserInvitationConfig(AppConfig):
 | 
			
		||||
    """passbook invitation stage config"""
 | 
			
		||||
 | 
			
		||||
    name = "passbook.stages.invitation"
 | 
			
		||||
    label = "passbook_stages_invitation"
 | 
			
		||||
    verbose_name = "passbook Stages.User Invitation"
 | 
			
		||||
							
								
								
									
										33
									
								
								passbook/stages/invitation/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								passbook/stages/invitation/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
"""passbook flows invitation forms"""
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.stages.invitation.models import Invitation, InvitationStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationStageForm(forms.ModelForm):
 | 
			
		||||
    """Form to create/edit InvitationStage instances"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = InvitationStage
 | 
			
		||||
        fields = ["name", "continue_flow_without_invitation"]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationForm(forms.ModelForm):
 | 
			
		||||
    """InvitationForm"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = Invitation
 | 
			
		||||
        fields = ["expires", "fixed_data"]
 | 
			
		||||
        labels = {
 | 
			
		||||
            "fixed_data": _("Optional fixed data to enforce on user enrollment."),
 | 
			
		||||
        }
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "fixed_username": forms.TextInput(),
 | 
			
		||||
            "fixed_email": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										72
									
								
								passbook/stages/invitation/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								passbook/stages/invitation/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
# Generated by Django 3.0.5 on 2020-05-11 19:09
 | 
			
		||||
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields.jsonb
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_flows", "0004_auto_20200510_2310"),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="InvitationStage",
 | 
			
		||||
            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": "Invitation Stage",
 | 
			
		||||
                "verbose_name_plural": "Invitation Stages",
 | 
			
		||||
            },
 | 
			
		||||
            bases=("passbook_flows.stage",),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Invitation",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "uuid",
 | 
			
		||||
                    models.UUIDField(
 | 
			
		||||
                        default=uuid.uuid4,
 | 
			
		||||
                        editable=False,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("expires", models.DateTimeField(blank=True, default=None, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "fixed_data",
 | 
			
		||||
                    django.contrib.postgres.fields.jsonb.JSONField(default=dict),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "created_by",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Invitation",
 | 
			
		||||
                "verbose_name_plural": "Invitations",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
# Generated by Django 3.0.5 on 2020-05-11 19:46
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_stages_invitation", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="invitationstage",
 | 
			
		||||
            name="continue_flow_without_invitation",
 | 
			
		||||
            field=models.BooleanField(
 | 
			
		||||
                default=False,
 | 
			
		||||
                help_text="If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/stages/invitation/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/invitation/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										50
									
								
								passbook/stages/invitation/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								passbook/stages/invitation/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
"""invitation stage models"""
 | 
			
		||||
from django.contrib.postgres.fields import JSONField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.flows.models import Stage
 | 
			
		||||
from passbook.lib.models import UUIDModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationStage(Stage):
 | 
			
		||||
    """Invitation stage, to enroll themselves with enforced parameters"""
 | 
			
		||||
 | 
			
		||||
    continue_flow_without_invitation = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "If this flag is set, this Stage will jump to the next Stage when "
 | 
			
		||||
                "no Invitation is given. By default this Stage will cancel the "
 | 
			
		||||
                "Flow when no invitation is given."
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    type = "passbook.stages.invitation.stage.InvitationStageView"
 | 
			
		||||
    form = "passbook.stages.invitation.forms.InvitationStageForm"
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"Invitation Stage {self.name}"
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Invitation Stage")
 | 
			
		||||
        verbose_name_plural = _("Invitation Stages")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Invitation(UUIDModel):
 | 
			
		||||
    """Single-use invitation link"""
 | 
			
		||||
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
			
		||||
    expires = models.DateTimeField(default=None, blank=True, null=True)
 | 
			
		||||
    fixed_data = JSONField(default=dict)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"Invitation {self.uuid.hex} created by {self.created_by}"
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Invitation")
 | 
			
		||||
        verbose_name_plural = _("Invitations")
 | 
			
		||||
							
								
								
									
										26
									
								
								passbook/stages/invitation/stage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/stages/invitation/stage.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
"""invitation stage logic"""
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
 | 
			
		||||
from passbook.flows.stage import AuthenticationStage
 | 
			
		||||
from passbook.stages.invitation.models import Invitation, InvitationStage
 | 
			
		||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
			
		||||
 | 
			
		||||
INVITATION_TOKEN_KEY = "token"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationStageView(AuthenticationStage):
 | 
			
		||||
    """Finalise Authentication flow by logging the user in"""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        stage: InvitationStage = self.executor.current_stage
 | 
			
		||||
        if INVITATION_TOKEN_KEY not in request.GET:
 | 
			
		||||
            # No Invitation was given, raise error or continue
 | 
			
		||||
            if stage.continue_flow_without_invitation:
 | 
			
		||||
                return self.executor.stage_ok()
 | 
			
		||||
            return self.executor.stage_invalid()
 | 
			
		||||
 | 
			
		||||
        token = request.GET[INVITATION_TOKEN_KEY]
 | 
			
		||||
        invite: Invitation = get_object_or_404(Invitation, pk=token)
 | 
			
		||||
        self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data
 | 
			
		||||
        return self.executor.stage_ok()
 | 
			
		||||
							
								
								
									
										83
									
								
								passbook/stages/invitation/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								passbook/stages/invitation/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
"""login tests"""
 | 
			
		||||
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 PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
			
		||||
from passbook.flows.views import SESSION_KEY_PLAN
 | 
			
		||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
			
		||||
from passbook.stages.user_login.forms import UserLoginStageForm
 | 
			
		||||
from passbook.stages.user_login.models import UserLoginStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserLoginStage(TestCase):
 | 
			
		||||
    """Login tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.user = User.objects.create(username="unittest", email="test@beryju.org")
 | 
			
		||||
        self.client = Client()
 | 
			
		||||
 | 
			
		||||
        self.flow = Flow.objects.create(
 | 
			
		||||
            name="test-login",
 | 
			
		||||
            slug="test-login",
 | 
			
		||||
            designation=FlowDesignation.AUTHENTICATION,
 | 
			
		||||
        )
 | 
			
		||||
        self.stage = UserLoginStage.objects.create(name="login")
 | 
			
		||||
        FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
 | 
			
		||||
 | 
			
		||||
    def test_valid_password(self):
 | 
			
		||||
        """Test with a valid pending user and backend"""
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
 | 
			
		||||
        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
			
		||||
        plan.context[
 | 
			
		||||
            PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
			
		||||
        ] = "django.contrib.auth.backends.ModelBackend"
 | 
			
		||||
        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_core:overview"))
 | 
			
		||||
 | 
			
		||||
    def test_without_user(self):
 | 
			
		||||
        """Test a plan without any pending user, resulting in a denied"""
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, 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"))
 | 
			
		||||
 | 
			
		||||
    def test_without_backend(self):
 | 
			
		||||
        """Test a plan with pending user, without backend, resulting in a denied"""
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
 | 
			
		||||
        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
			
		||||
        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"))
 | 
			
		||||
 | 
			
		||||
    def test_form(self):
 | 
			
		||||
        """Test Form"""
 | 
			
		||||
        data = {"name": "test"}
 | 
			
		||||
        self.assertEqual(UserLoginStageForm(data).is_valid(), True)
 | 
			
		||||
		Reference in New Issue
	
	Block a user