stages/invitation: start extracting invitation from core
This commit is contained in:
parent
d49c58f326
commit
7500e622f6
|
@ -11,17 +11,17 @@ from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import DeleteView, ListView
|
from django.views.generic import DeleteView, ListView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
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.core.signals import invitation_created
|
||||||
from passbook.lib.views import CreateAssignPermView
|
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):
|
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||||
"""Show list of all invitations"""
|
"""Show list of all invitations"""
|
||||||
|
|
||||||
model = Invitation
|
model = Invitation
|
||||||
permission_required = "passbook_core.view_invitation"
|
permission_required = "passbook_stages_invitation.view_invitation"
|
||||||
template_name = "administration/invitation/list.html"
|
template_name = "administration/invitation/list.html"
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
ordering = "-expires"
|
ordering = "-expires"
|
||||||
|
@ -37,7 +37,7 @@ class InvitationCreateView(
|
||||||
|
|
||||||
model = Invitation
|
model = Invitation
|
||||||
form_class = InvitationForm
|
form_class = InvitationForm
|
||||||
permission_required = "passbook_core.add_invitation"
|
permission_required = "passbook_stages_invitation.add_invitation"
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
template_name = "generic/create.html"
|
||||||
success_url = reverse_lazy("passbook_admin:invitations")
|
success_url = reverse_lazy("passbook_admin:invitations")
|
||||||
|
@ -61,7 +61,7 @@ class InvitationDeleteView(
|
||||||
"""Delete invitation"""
|
"""Delete invitation"""
|
||||||
|
|
||||||
model = Invitation
|
model = Invitation
|
||||||
permission_required = "passbook_core.delete_invitation"
|
permission_required = "passbook_stages_invitation.delete_invitation"
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
template_name = "generic/delete.html"
|
||||||
success_url = reverse_lazy("passbook_admin:invitations")
|
success_url = reverse_lazy("passbook_admin:invitations")
|
||||||
|
|
|
@ -5,9 +5,10 @@ from django.views.generic import TemplateView
|
||||||
|
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
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.flows.models import Flow, Stage
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
|
from passbook.stages.invitation.models import Invitation
|
||||||
|
|
||||||
|
|
||||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||||
|
|
|
@ -11,7 +11,6 @@ from passbook.api.permissions import CustomObjectPermissions
|
||||||
from passbook.audit.api import EventViewSet
|
from passbook.audit.api import EventViewSet
|
||||||
from passbook.core.api.applications import ApplicationViewSet
|
from passbook.core.api.applications import ApplicationViewSet
|
||||||
from passbook.core.api.groups import GroupViewSet
|
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.policies import PolicyViewSet
|
||||||
from passbook.core.api.propertymappings import PropertyMappingViewSet
|
from passbook.core.api.propertymappings import PropertyMappingViewSet
|
||||||
from passbook.core.api.providers import ProviderViewSet
|
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.captcha.api import CaptchaStageViewSet
|
||||||
from passbook.stages.email.api import EmailStageViewSet
|
from passbook.stages.email.api import EmailStageViewSet
|
||||||
from passbook.stages.identification.api import IdentificationStageViewSet
|
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.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
|
||||||
|
@ -51,7 +51,6 @@ for _passbook_app in get_apps():
|
||||||
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
|
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
|
||||||
|
|
||||||
router.register("core/applications", ApplicationViewSet)
|
router.register("core/applications", ApplicationViewSet)
|
||||||
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)
|
||||||
|
|
||||||
|
@ -83,6 +82,8 @@ router.register("stages/all", StageViewSet)
|
||||||
router.register("stages/captcha", CaptchaStageViewSet)
|
router.register("stages/captcha", CaptchaStageViewSet)
|
||||||
router.register("stages/email", EmailStageViewSet)
|
router.register("stages/email", EmailStageViewSet)
|
||||||
router.register("stages/identification", IdentificationStageViewSet)
|
router.register("stages/identification", IdentificationStageViewSet)
|
||||||
|
router.register("stages/invitation", InvitationStageViewSet)
|
||||||
|
router.register("stages/invitation/invitations", InvitationViewSet)
|
||||||
router.register("stages/otp", OTPStageViewSet)
|
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)
|
||||||
|
|
|
@ -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(),
|
|
||||||
}
|
|
|
@ -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.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_prometheus.models import ExportModelOperationsMixin
|
from django_prometheus.models import ExportModelOperationsMixin
|
||||||
|
@ -196,30 +195,6 @@ class Policy(ExportModelOperationsMixin("policy"), UUIDModel, CreatedUpdatedMode
|
||||||
raise PolicyException()
|
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):
|
class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
|
||||||
"""One-time link for password resets/sign-up-confirmations"""
|
"""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:
|
try:
|
||||||
target = reverse(view, kwargs=kwargs)
|
target = reverse(view, kwargs=kwargs)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
if not is_url_absolute(view):
|
||||||
|
return redirect(view)
|
||||||
LOGGER.debug("redirect target is not a valid view", view=view)
|
LOGGER.debug("redirect target is not a valid view", view=view)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -108,6 +108,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.invitation.apps.PassbookStageUserInvitationConfig",
|
||||||
"passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
|
"passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
|
||||||
"passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig",
|
"passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig",
|
||||||
"passbook.stages.user_write.apps.PassbookStageUserWriteConfig",
|
"passbook.stages.user_write.apps.PassbookStageUserWriteConfig",
|
||||||
|
|
|
@ -1,8 +1,28 @@
|
||||||
"""Invitation API Views"""
|
"""Invitation Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
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):
|
class InvitationSerializer(ModelSerializer):
|
|
@ -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"
|
|
@ -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(),
|
||||||
|
}
|
|
@ -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,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")
|
|
@ -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()
|
|
@ -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