From dce869b5665c8118c61fee9b209f77b9cdda372b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 24 Mar 2021 11:57:56 +0100 Subject: [PATCH] flows: fix post-email continuation not working Signed-off-by: Jens Langhammer --- authentik/flows/challenge.py | 2 +- authentik/flows/stage.py | 18 +++++++++++++++++- authentik/flows/views.py | 4 ++-- authentik/stages/email/stage.py | 7 ++++++- authentik/stages/email/tests/test_stage.py | 19 ++++++++++++++++--- tests/e2e/utils.py | 4 +++- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index b4a53e7b8..c03fb3ffc 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -46,7 +46,7 @@ class Challenge(Serializer): background = CharField(required=False) response_errors = DictField( - child=ErrorDetailSerializer(many=True), allow_empty=False, required=False + child=ErrorDetailSerializer(many=True), allow_empty=True, required=False ) def create(self, validated_data: dict) -> Model: diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index 0f59e9c56..8186aaace 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -16,11 +16,27 @@ from authentik.flows.challenge import ( ) from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.views import FlowExecutorView +from authentik.lib.sentry import SentryIgnoredException PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" LOGGER = get_logger() +class InvalidChallengeError(SentryIgnoredException): + """Error raised when a challenge from a stage is not valid""" + + def __init__(self, errors, stage_view: View, challenge: Challenge) -> None: + super().__init__() + self.errors = errors + self.stage_view = stage_view + self.challenge = challenge + + def __str__(self) -> str: + return ( + f"Invalid challenge from {self.stage_view}: {self.errors}\n{self.challenge}" + ) + + class StageView(View): """Abstract Stage, inherits TemplateView but can be combined with FormView""" @@ -64,7 +80,7 @@ class ChallengeStageView(StageView): """Return a challenge for the frontend to solve""" challenge = self._get_challenge(*args, **kwargs) if not challenge.is_valid(): - LOGGER.warning(challenge.errors) + LOGGER.warning(challenge.errors, stage_view=self, challenge=challenge) return HttpChallengeResponse(challenge) # pylint: disable=unused-argument diff --git a/authentik/flows/views.py b/authentik/flows/views.py index cdda12c4d..59664fbe1 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -103,8 +103,8 @@ class FlowExecutorView(APIView): # To match behaviour with loading an empty flow plan from cache, # we don't show an error message here, but rather call _flow_done() return self._flow_done() - # Initial flow request, check if we have an upstream query string passed in - request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) + # Initial flow request, check if we have an upstream query string passed in + request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) # We don't save the Plan after getting the next stage # as it hasn't been successfully passed yet next_stage = self.plan.next(self.request) diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 162e14ee1..03c2e5086 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -84,6 +84,7 @@ class EmailStageView(ChallengeStageView): messages.success(request, _("Successfully verified Email.")) return self.executor.stage_ok() if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: + LOGGER.debug("No pending user") messages.error(self.request, _("No pending user.")) return self.executor.stage_invalid() # Check if we've already sent the initial e-mail @@ -94,7 +95,11 @@ class EmailStageView(ChallengeStageView): def get_challenge(self) -> Challenge: challenge = EmailChallenge( - data={"type": ChallengeTypes.native.value, "component": "ak-stage-email"} + data={ + "type": ChallengeTypes.native.value, + "component": "ak-stage-email", + "title": "Email sent.", + } ) return challenge diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py index 42c998abc..432ee5f24 100644 --- a/authentik/stages/email/tests/test_stage.py +++ b/authentik/stages/email/tests/test_stage.py @@ -5,12 +5,13 @@ from django.core import mail from django.test import Client, TestCase from django.urls import reverse from django.utils.encoding import force_str +from django.utils.http import urlencode from authentik.core.models import Token, User from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan -from authentik.flows.views import SESSION_KEY_GET, SESSION_KEY_PLAN +from authentik.flows.views import SESSION_KEY_PLAN from authentik.stages.email.models import EmailStage from authentik.stages.email.stage import QS_KEY_TOKEN @@ -104,11 +105,23 @@ class TestEmailStage(TestCase): ) session = self.client.session session[SESSION_KEY_PLAN] = plan - token: Token = Token.objects.get(user=self.user) - session[SESSION_KEY_GET] = {QS_KEY_TOKEN: token.key} session.save() + token: Token = Token.objects.get(user=self.user) with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): + # Call the executor shell to preseed the session + url = reverse( + "authentik_api:flow-executor", + kwargs={"flow_slug": self.flow.slug}, + ) + url_query = urlencode( + { + QS_KEY_TOKEN: token.key, + } + ) + url += f"?query={url_query}" + self.client.get(url) + # Call the actual executor to get the JSON Response response = self.client.get( reverse( diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 8e5eb81a8..f6314db79 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -89,10 +89,12 @@ class SeleniumTestCase(StaticLiveServerTestCase): ) self.driver.save_screenshot(screenshot_file) self.logger.warning("Saved screenshot", file=screenshot_file) + self.logger.debug("--------browser logs") for line in self.driver.get_log("browser"): - self.logger.warning( + self.logger.debug( line["message"], source=line["source"], level=line["level"] ) + self.logger.debug("--------end browser logs") if self.container: self.container.kill() self.driver.quit()