stages/invitation: delete invite only after full enrollment flow is completed

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-08-01 13:21:27 +02:00
parent fe629f8b51
commit 26e66969c9
2 changed files with 40 additions and 5 deletions

View File

@ -1,18 +1,23 @@
"""invitation stage logic""" """invitation stage logic"""
from copy import deepcopy
from typing import Optional from typing import Optional
from deepmerge import always_merger
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from structlog.stdlib import get_logger
from authentik.flows.models import in_memory_stage
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views import SESSION_KEY_GET from authentik.flows.views import SESSION_KEY_GET
from authentik.stages.invitation.models import Invitation, InvitationStage from authentik.stages.invitation.models import Invitation, InvitationStage
from authentik.stages.invitation.signals import invitation_used from authentik.stages.invitation.signals import invitation_used
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger()
INVITATION_TOKEN_KEY = "token" # nosec INVITATION_TOKEN_KEY = "token" # nosec
INVITATION_IN_EFFECT = "invitation_in_effect" INVITATION_IN_EFFECT = "invitation_in_effect"
INVITATION = "invitation"
class InvitationStageView(StageView): class InvitationStageView(StageView):
@ -39,9 +44,37 @@ class InvitationStageView(StageView):
return self.executor.stage_invalid() return self.executor.stage_invalid()
invite: Invitation = get_object_or_404(Invitation, pk=token) invite: Invitation = get_object_or_404(Invitation, pk=token)
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data)
self.executor.plan.context[INVITATION_IN_EFFECT] = True self.executor.plan.context[INVITATION_IN_EFFECT] = True
self.executor.plan.context[INVITATION] = invite
context = {}
always_merger.merge(
context, self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
)
always_merger.merge(context, invite.fixed_data)
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = context
invitation_used.send(sender=self, request=request, invitation=invite) invitation_used.send(sender=self, request=request, invitation=invite)
if invite.single_use: if invite.single_use:
invite.delete() self.executor.plan.append_stage(in_memory_stage(InvitationFinalStageView))
return self.executor.stage_ok()
class InvitationFinalStageView(StageView):
"""Final stage which is injected by invitation stage. Deletes
the used invitation."""
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Call get as this request may be called with post"""
return self.get(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Delete invitation if single_use is active"""
invitation: Invitation = self.executor.plan.context.get(INVITATION, None)
if not invitation:
LOGGER.warning("InvitationFinalStageView stage called without invitation")
return HttpResponseBadRequest
if not invitation.single_use:
return self.executor.stage_ok()
invitation.delete()
return self.executor.stage_ok() return self.executor.stage_ok()

View File

@ -156,11 +156,13 @@ class TestUserLoginStage(TestCase):
base_url = reverse( base_url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
) )
response = self.client.get(base_url) response = self.client.get(base_url, follow=True)
session = self.client.session session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN] plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) self.assertEqual(
plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT]
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual( self.assertJSONEqual(