Exemple #1
0
def test_many_polymorphic_serializer_extend_schema(no_warnings, explicit):
    if explicit:
        proxy_serializer = serializers.ListSerializer(
            child=PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS))
    else:
        proxy_serializer = PolymorphicProxySerializer(**
                                                      PROXY_SERIALIZER_PARAMS,
                                                      many=True)

    @extend_schema(request=proxy_serializer, responses=proxy_serializer)
    @api_view(['POST'])
    def view_func(request, format=None):
        pass  # pragma: no cover

    schema = generate_schema('/x/', view_function=view_func)
    assert 'MetaPerson' in schema['components']['schemas']
    op = schema['paths']['/x/']['post']
    assert get_response_schema(op) == {
        'type': 'array',
        'items': {
            '$ref': '#/components/schemas/MetaPerson'
        }
    }
    assert get_request_schema(op) == {
        'type': 'array',
        'items': {
            '$ref': '#/components/schemas/MetaPerson'
        }
    }
Exemple #2
0
def test_polymorphic_proxy_serializer_misusage(no_warnings):
    with pytest.raises(AssertionError):
        PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS).data

    with pytest.raises(AssertionError):
        PolymorphicProxySerializer(
            **PROXY_SERIALIZER_PARAMS).to_representation(None)

    with pytest.raises(AssertionError):
        PolymorphicProxySerializer(
            **PROXY_SERIALIZER_PARAMS).to_internal_value(None)
 class PersonViewSet(viewsets.GenericViewSet):
     @extend_schema(
         request=PolymorphicProxySerializer(
             component_name='MetaPerson',
             serializers=[LegalPersonSerializer, NaturalPersonSerializer],
             resource_type_field_name='type',
         ),
         responses=PolymorphicProxySerializer(
             component_name='MetaPerson',
             serializers=[LegalPersonSerializer, NaturalPersonSerializer],
             resource_type_field_name='type',
         ))
     def create(self, request, *args, **kwargs):
         return Response({})
def test_stripped_down_polymorphic_serializer(no_warnings):
    @extend_schema_field(
        PolymorphicProxySerializer(
            component_name='MetaPerson',
            serializers=[LegalPersonSerializer, NaturalPersonSerializer],
            resource_type_field_name=None,
        ))
    class XField(serializers.DictField):
        pass  # pragma: no cover

    class XSerializer(serializers.Serializer):
        field = XField()

    @extend_schema(request=XSerializer, responses=XSerializer)
    @api_view(['GET'])
    def view_func(request, format=None):
        pass  # pragma: no cover

    schema = generate_schema('x', view_function=view_func)
    assert schema['components']['schemas']['MetaPerson'] == {
        'oneOf': [{
            '$ref': '#/components/schemas/LegalPerson'
        }, {
            '$ref': '#/components/schemas/NaturalPerson'
        }]
    }
 class XAPIView(APIView):
     @extend_schema(responses=PolymorphicProxySerializer(
         component_name='Broken',
         serializers=[IncompleteSerializer],
         resource_type_field_name='type',
     ))
     def get(self, request):
         pass  # pragma: no cover
    class XSerializer(serializers.Serializer):
        field = serializers.SerializerMethodField()

        @extend_schema_field(
            PolymorphicProxySerializer(
                component_name='MetaPerson',
                serializers=[LegalPersonSerializer, NaturalPersonSerializer],
                resource_type_field_name='type',
            ))
        def get_field(self, request):
            pass  # pragma: no cover
Exemple #7
0
def test_many_polymorphic_proxy_serializer_extend_schema_field(
        no_warnings, explicit):
    if explicit:
        proxy_serializer = serializers.ListField(
            child=PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS))
    else:
        proxy_serializer = PolymorphicProxySerializer(**
                                                      PROXY_SERIALIZER_PARAMS,
                                                      many=True)

    @extend_schema_field(proxy_serializer)
    class XField(serializers.DictField):
        pass  # pragma: no cover

    class XSerializer(serializers.Serializer):
        field = XField()

    @extend_schema(request=XSerializer, responses=XSerializer)
    @api_view(['POST'])
    def view_func(request, format=None):
        pass  # pragma: no cover

    schema = generate_schema('/x/', view_function=view_func)
    assert 'MetaPerson' in schema['components']['schemas']
    assert schema['components']['schemas']['X'] == {
        'type': 'object',
        'properties': {
            'field': {
                'type': 'array',
                'items': {
                    '$ref': '#/components/schemas/MetaPerson'
                }
            }
        },
        'required': ['field']
    }
    op = schema['paths']['/x/']['post']
    assert get_request_schema(op) == {'$ref': '#/components/schemas/X'}
    assert get_response_schema(op) == {'$ref': '#/components/schemas/X'}
def test_polymorphic_serializer_as_field_via_extend_schema_field(no_warnings):
    @extend_schema_field(
        PolymorphicProxySerializer(
            component_name='MetaPerson',
            serializers=[LegalPersonSerializer, NaturalPersonSerializer],
            resource_type_field_name='type',
        ))
    class XField(serializers.DictField):
        pass  # pragma: no cover

    class XSerializer(serializers.Serializer):
        field = XField()

    @extend_schema(request=XSerializer, responses=XSerializer)
    @api_view(['GET'])
    def view_func(request, format=None):
        pass  # pragma: no cover

    schema = generate_schema('x', view_function=view_func)
    assert 'MetaPerson' in schema['components']['schemas']
    assert 'MetaPerson' in schema['components']['schemas']['X']['properties'][
        'field']['$ref']
Exemple #9
0
class FlowExecutorView(APIView):
    """Stage 1 Flow executor, passing requests to Stage Views"""

    permission_classes = [AllowAny]

    flow: Flow

    plan: Optional[FlowPlan] = None
    current_binding: FlowStageBinding
    current_stage: Stage
    current_stage_view: View

    _logger: BoundLogger

    def setup(self, request: HttpRequest, flow_slug: str):
        super().setup(request, flow_slug=flow_slug)
        self.flow = get_object_or_404(Flow.objects.select_related(),
                                      slug=flow_slug)
        self._logger = get_logger().bind(flow_slug=flow_slug)
        set_tag("authentik.flow", self.flow.slug)

    def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
        """When a flow is non-applicable check if user is on the correct domain"""
        if NEXT_ARG_NAME in self.request.GET:
            if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
                self._logger.debug("f(exec): Redirecting to next on fail")
                return redirect(self.request.GET.get(NEXT_ARG_NAME))
        message = exc.__doc__ if exc.__doc__ else str(exc)
        return self.stage_invalid(error_message=message)

    def _check_flow_token(self, get_params: QueryDict):
        """Check if the user is using a flow token to restore a plan"""
        tokens = FlowToken.filter_not_expired(key=get_params[QS_KEY_TOKEN])
        if not tokens.exists():
            return False
        token: FlowToken = tokens.first()
        try:
            plan = token.plan
        except (AttributeError, EOFError, ImportError, IndexError) as exc:
            LOGGER.warning("f(exec): Failed to restore token plan", exc=exc)
        finally:
            token.delete()
        if not isinstance(plan, FlowPlan):
            return None
        plan.context[PLAN_CONTEXT_IS_RESTORED] = True
        self._logger.debug("f(exec): restored flow plan from token", plan=plan)
        return plan

    # pylint: disable=unused-argument, too-many-return-statements
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
        with Hub.current.start_span(op="authentik.flow.executor.dispatch",
                                    description=self.flow.slug) as span:
            span.set_data("authentik Flow", self.flow.slug)
            get_params = QueryDict(request.GET.get("query", ""))
            if QS_KEY_TOKEN in get_params:
                plan = self._check_flow_token(get_params)
                if plan:
                    self.request.session[SESSION_KEY_PLAN] = plan
            # Early check if there's an active Plan for the current session
            if SESSION_KEY_PLAN in self.request.session:
                self.plan = self.request.session[SESSION_KEY_PLAN]
                if self.plan.flow_pk != self.flow.pk.hex:
                    self._logger.warning(
                        "f(exec): Found existing plan for other flow, deleting plan",
                    )
                    # Existing plan is deleted from session and instance
                    self.plan = None
                    self.cancel()
                self._logger.debug("f(exec): Continuing existing plan")

            # Don't check session again as we've either already loaded the plan or we need to plan
            if not self.plan:
                request.session[SESSION_KEY_HISTORY] = []
                self._logger.debug(
                    "f(exec): No active Plan found, initiating planner")
                try:
                    self.plan = self._initiate_plan()
                except FlowNonApplicableException as exc:
                    self._logger.warning(
                        "f(exec): Flow not applicable to current user",
                        exc=exc)
                    return to_stage_response(self.request,
                                             self.handle_invalid_flow(exc))
                except EmptyFlowException as exc:
                    self._logger.warning("f(exec): Flow is empty", exc=exc)
                    # 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] = get_params
            # We don't save the Plan after getting the next stage
            # as it hasn't been successfully passed yet
            try:
                # This is the first time we actually access any attribute on the selected plan
                # if the cached plan is from an older version, it might have different attributes
                # in which case we just delete the plan and invalidate everything
                next_binding = self.plan.next(self.request)
            except Exception as exc:  # pylint: disable=broad-except
                self._logger.warning(
                    "f(exec): found incompatible flow plan, invalidating run",
                    exc=exc)
                keys = cache.keys("flow_*")
                cache.delete_many(keys)
                return self.stage_invalid()
            if not next_binding:
                self._logger.debug("f(exec): no more stages, flow is done.")
                return self._flow_done()
            self.current_binding = next_binding
            self.current_stage = next_binding.stage
            self._logger.debug(
                "f(exec): Current stage",
                current_stage=self.current_stage,
                flow_slug=self.flow.slug,
            )
            try:
                stage_cls = self.current_stage.type
            except NotImplementedError as exc:
                self._logger.debug("Error getting stage type", exc=exc)
                return self.stage_invalid()
            self.current_stage_view = stage_cls(self)
            self.current_stage_view.args = self.args
            self.current_stage_view.kwargs = self.kwargs
            self.current_stage_view.request = request
            try:
                return super().dispatch(request)
            except InvalidStageError as exc:
                return self.stage_invalid(str(exc))

    def handle_exception(self, exc: Exception) -> HttpResponse:
        """Handle exception in stage execution"""
        if settings.DEBUG or settings.TEST:
            raise exc
        capture_exception(exc)
        self._logger.warning(exc)
        Event.new(
            action=EventAction.SYSTEM_EXCEPTION,
            message=exception_to_string(exc),
        ).from_http(self.request)
        return to_stage_response(self.request,
                                 FlowErrorResponse(self.request, exc))

    @extend_schema(
        responses={
            200:
            PolymorphicProxySerializer(
                component_name="ChallengeTypes",
                serializers=challenge_types(),
                resource_type_field_name="component",
            ),
        },
        request=OpenApiTypes.NONE,
        parameters=[
            OpenApiParameter(
                name="query",
                location=OpenApiParameter.QUERY,
                required=True,
                description="Querystring as received",
                type=OpenApiTypes.STR,
            )
        ],
        operation_id="flows_executor_get",
    )
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
        """Get the next pending challenge from the currently active flow."""
        self._logger.debug(
            "f(exec): Passing GET",
            view_class=class_to_path(self.current_stage_view.__class__),
            stage=self.current_stage,
        )
        try:
            with Hub.current.start_span(
                    op="authentik.flow.executor.stage",
                    description=class_to_path(
                        self.current_stage_view.__class__),
            ) as span:
                span.set_data("Method", "GET")
                span.set_data("authentik Stage", self.current_stage_view)
                span.set_data("authentik Flow", self.flow.slug)
                stage_response = self.current_stage_view.get(
                    request, *args, **kwargs)
                return to_stage_response(request, stage_response)
        except Exception as exc:  # pylint: disable=broad-except
            return self.handle_exception(exc)

    @extend_schema(
        responses={
            200:
            PolymorphicProxySerializer(
                component_name="ChallengeTypes",
                serializers=challenge_types(),
                resource_type_field_name="component",
            ),
        },
        request=PolymorphicProxySerializer(
            component_name="FlowChallengeResponse",
            serializers=challenge_response_types(),
            resource_type_field_name="component",
        ),
        parameters=[
            OpenApiParameter(
                name="query",
                location=OpenApiParameter.QUERY,
                required=True,
                description="Querystring as received",
                type=OpenApiTypes.STR,
            )
        ],
        operation_id="flows_executor_solve",
    )
    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
        """Solve the previously retrieved challenge and advanced to the next stage."""
        self._logger.debug(
            "f(exec): Passing POST",
            view_class=class_to_path(self.current_stage_view.__class__),
            stage=self.current_stage,
        )
        try:
            with Hub.current.start_span(
                    op="authentik.flow.executor.stage",
                    description=class_to_path(
                        self.current_stage_view.__class__),
            ) as span:
                span.set_data("Method", "POST")
                span.set_data("authentik Stage", self.current_stage_view)
                span.set_data("authentik Flow", self.flow.slug)
                stage_response = self.current_stage_view.post(
                    request, *args, **kwargs)
                return to_stage_response(request, stage_response)
        except Exception as exc:  # pylint: disable=broad-except
            return self.handle_exception(exc)

    def _initiate_plan(self) -> FlowPlan:
        planner = FlowPlanner(self.flow)
        plan = planner.plan(self.request)
        self.request.session[SESSION_KEY_PLAN] = plan
        try:
            # Call the has_stages getter to check that
            # there are no issues with the class we might've gotten
            # from the cache. If there are errors, just delete all cached flows
            _ = plan.has_stages
        except Exception:  # pylint: disable=broad-except
            keys = cache.keys("flow_*")
            cache.delete_many(keys)
            return self._initiate_plan()
        return plan

    def restart_flow(self, keep_context=False) -> HttpResponse:
        """Restart the currently active flow, optionally keeping the current context"""
        planner = FlowPlanner(self.flow)
        default_context = None
        if keep_context:
            default_context = self.plan.context
        plan = planner.plan(self.request, default_context)
        self.request.session[SESSION_KEY_PLAN] = plan
        kwargs = self.kwargs
        kwargs.update({"flow_slug": self.flow.slug})
        return redirect_with_qs("authentik_api:flow-executor",
                                self.request.GET, **kwargs)

    def _flow_done(self) -> HttpResponse:
        """User Successfully passed all stages"""
        # Since this is wrapped by the ExecutorShell, the next argument is saved in the session
        # extract the next param before cancel as that cleans it
        if self.plan and PLAN_CONTEXT_REDIRECT in self.plan.context:
            # The context `redirect` variable can only be set by
            # an expression policy or authentik itself, so we don't
            # check if its an absolute URL or a relative one
            self.cancel()
            return redirect(self.plan.context.get(PLAN_CONTEXT_REDIRECT))
        next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
            NEXT_ARG_NAME, "authentik_core:root-redirect")
        self.cancel()
        return to_stage_response(self.request, redirect_with_qs(next_param))

    def stage_ok(self) -> HttpResponse:
        """Callback called by stages upon successful completion.
        Persists updated plan and context to session."""
        self._logger.debug(
            "f(exec): Stage ok",
            stage_class=class_to_path(self.current_stage_view.__class__),
        )
        self.request.session.get(SESSION_KEY_HISTORY,
                                 []).append(deepcopy(self.plan))
        self.plan.pop()
        self.request.session[SESSION_KEY_PLAN] = self.plan
        if self.plan.bindings:
            self._logger.debug(
                "f(exec): Continuing with next stage",
                remaining=len(self.plan.bindings),
            )
            kwargs = self.kwargs
            kwargs.update({"flow_slug": self.flow.slug})
            return redirect_with_qs("authentik_api:flow-executor",
                                    self.request.GET, **kwargs)
        # User passed all stages
        self._logger.debug(
            "f(exec): User passed all stages",
            context=cleanse_dict(self.plan.context),
        )
        return self._flow_done()

    def stage_invalid(self,
                      error_message: Optional[str] = None) -> HttpResponse:
        """Callback used stage when data is correct but a policy denies access
        or the user account is disabled.

        Optionally, an exception can be passed, which will be shown if the current user
        is a superuser."""
        self._logger.debug("f(exec): Stage invalid")
        self.cancel()
        challenge_view = AccessDeniedChallengeView(self, error_message)
        challenge_view.request = self.request
        return to_stage_response(self.request,
                                 challenge_view.get(self.request))

    def cancel(self):
        """Cancel current execution and return a redirect"""
        keys_to_delete = [
            SESSION_KEY_APPLICATION_PRE,
            SESSION_KEY_PLAN,
            SESSION_KEY_GET,
            # We don't delete the history on purpose, as a user might
            # still be inspecting it.
            # It's only deleted on a fresh executions
            # SESSION_KEY_HISTORY,
        ]
        for key in keys_to_delete:
            if key in self.request.session:
                del self.request.session[key]
class NaturalPersonSerializer(serializers.ModelSerializer):
    type = serializers.SerializerMethodField()

    class Meta:
        model = NaturalPerson2
        fields = ('id', 'first_name', 'last_name', 'type')

    def get_type(self, obj) -> str:
        return 'natural'


with mock.patch('rest_framework.settings.api_settings.DEFAULT_SCHEMA_CLASS',
                AutoSchema):
    implicit_poly_proxy = PolymorphicProxySerializer(
        component_name='MetaPerson',
        serializers=[LegalPersonSerializer, NaturalPersonSerializer],
        resource_type_field_name='type',
    )

    class ImplicitPersonViewSet(viewsets.GenericViewSet):
        @extend_schema(request=implicit_poly_proxy,
                       responses=implicit_poly_proxy)
        def create(self, request, *args, **kwargs):
            return Response({})  # pragma: no cover

        @extend_schema(
            request=implicit_poly_proxy,
            responses=implicit_poly_proxy,
            parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)],
        )
        def partial_update(self, request, *args, **kwargs):
Exemple #11
0
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate

LOGGER = get_logger()


@extend_schema_field(
    PolymorphicProxySerializer(
        component_name="LoginChallengeTypes",
        serializers={
            RedirectChallenge().fields["component"].default: RedirectChallenge,
            PlexAuthenticationChallenge().fields["component"].default:
            PlexAuthenticationChallenge,
            AppleLoginChallenge().fields["component"].default:
            AppleLoginChallenge,
        },
        resource_type_field_name="component",
    ))
class ChallengeDictWrapper(DictField):
    """Wrapper around DictField that annotates itself as challenge proxy"""


class LoginSourceSerializer(PassiveSerializer):
    """Serializer for Login buttons of sources"""

    name = CharField()
    icon_url = CharField(required=False, allow_null=True)
Exemple #12
0
class FlowExecutorView(APIView):
    """Stage 1 Flow executor, passing requests to Stage Views"""

    permission_classes = [AllowAny]

    flow: Flow

    plan: Optional[FlowPlan] = None
    current_stage: Stage
    current_stage_view: View

    _logger: BoundLogger

    def setup(self, request: HttpRequest, flow_slug: str):
        super().setup(request, flow_slug=flow_slug)
        self.flow = get_object_or_404(Flow.objects.select_related(),
                                      slug=flow_slug)
        self._logger = get_logger().bind(flow_slug=flow_slug)

    def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
        """When a flow is non-applicable check if user is on the correct domain"""
        if NEXT_ARG_NAME in self.request.GET:
            if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
                self._logger.debug("f(exec): Redirecting to next on fail")
                return redirect(self.request.GET.get(NEXT_ARG_NAME))
        message = exc.__doc__ if exc.__doc__ else str(exc)
        return self.stage_invalid(error_message=message)

    # pylint: disable=unused-argument
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
        # Early check if theres an active Plan for the current session
        if SESSION_KEY_PLAN in self.request.session:
            self.plan = self.request.session[SESSION_KEY_PLAN]
            if self.plan.flow_pk != self.flow.pk.hex:
                self._logger.warning(
                    "f(exec): Found existing plan for other flow, deleteing plan",
                )
                # Existing plan is deleted from session and instance
                self.plan = None
                self.cancel()
            self._logger.debug("f(exec): Continuing existing plan")

        # Don't check session again as we've either already loaded the plan or we need to plan
        if not self.plan:
            self._logger.debug(
                "f(exec): No active Plan found, initiating planner")
            try:
                self.plan = self._initiate_plan()
            except FlowNonApplicableException as exc:
                self._logger.warning(
                    "f(exec): Flow not applicable to current user", exc=exc)
                return to_stage_response(self.request,
                                         self.handle_invalid_flow(exc))
            except EmptyFlowException as exc:
                self._logger.warning("f(exec): Flow is empty", exc=exc)
                # 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", ""))
        # 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)
        if not next_stage:
            self._logger.debug("f(exec): no more stages, flow is done.")
            return self._flow_done()
        self.current_stage = next_stage
        self._logger.debug(
            "f(exec): Current stage",
            current_stage=self.current_stage,
            flow_slug=self.flow.slug,
        )
        stage_cls = self.current_stage.type
        self.current_stage_view = stage_cls(self)
        self.current_stage_view.args = self.args
        self.current_stage_view.kwargs = self.kwargs
        self.current_stage_view.request = request
        return super().dispatch(request)

    @extend_schema(
        responses={
            200:
            PolymorphicProxySerializer(
                component_name="FlowChallengeRequest",
                serializers=challenge_types(),
                resource_type_field_name="component",
            ),
            404:
            OpenApiResponse(description="No Token found"
                            ),  # This error can be raised by the email stage
        },
        request=OpenApiTypes.NONE,
        parameters=[
            OpenApiParameter(
                name="query",
                location=OpenApiParameter.QUERY,
                required=True,
                description="Querystring as received",
                type=OpenApiTypes.STR,
            )
        ],
        operation_id="flows_executor_get",
    )
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
        """Get the next pending challenge from the currently active flow."""
        self._logger.debug(
            "f(exec): Passing GET",
            view_class=class_to_path(self.current_stage_view.__class__),
            stage=self.current_stage,
        )
        try:
            stage_response = self.current_stage_view.get(
                request, *args, **kwargs)
            return to_stage_response(request, stage_response)
        except Exception as exc:  # pylint: disable=broad-except
            capture_exception(exc)
            self._logger.warning(exc)
            return to_stage_response(request, FlowErrorResponse(request, exc))

    @extend_schema(
        responses={
            200:
            PolymorphicProxySerializer(
                component_name="FlowChallengeRequest",
                serializers=challenge_types(),
                resource_type_field_name="component",
            ),
        },
        request=PolymorphicProxySerializer(
            component_name="FlowChallengeResponse",
            serializers=challenge_response_types(),
            resource_type_field_name="component",
        ),
        parameters=[
            OpenApiParameter(
                name="query",
                location=OpenApiParameter.QUERY,
                required=True,
                description="Querystring as received",
                type=OpenApiTypes.STR,
            )
        ],
        operation_id="flows_executor_solve",
    )
    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
        """Solve the previously retrieved challenge and advanced to the next stage."""
        self._logger.debug(
            "f(exec): Passing POST",
            view_class=class_to_path(self.current_stage_view.__class__),
            stage=self.current_stage,
        )
        try:
            stage_response = self.current_stage_view.post(
                request, *args, **kwargs)
            return to_stage_response(request, stage_response)
        except Exception as exc:  # pylint: disable=broad-except
            capture_exception(exc)
            self._logger.warning(exc)
            return to_stage_response(request, FlowErrorResponse(request, exc))

    def _initiate_plan(self) -> FlowPlan:
        planner = FlowPlanner(self.flow)
        plan = planner.plan(self.request)
        self.request.session[SESSION_KEY_PLAN] = plan
        return plan

    def _flow_done(self) -> HttpResponse:
        """User Successfully passed all stages"""
        # Since this is wrapped by the ExecutorShell, the next argument is saved in the session
        # extract the next param before cancel as that cleans it
        next_param = None
        if self.plan:
            next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
        if not next_param:
            next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
                NEXT_ARG_NAME, "authentik_core:root-redirect")
        self.cancel()
        return to_stage_response(self.request, redirect_with_qs(next_param))

    def stage_ok(self) -> HttpResponse:
        """Callback called by stages upon successful completion.
        Persists updated plan and context to session."""
        self._logger.debug(
            "f(exec): Stage ok",
            stage_class=class_to_path(self.current_stage_view.__class__),
        )
        self.plan.pop()
        self.request.session[SESSION_KEY_PLAN] = self.plan
        if self.plan.stages:
            self._logger.debug(
                "f(exec): Continuing with next stage",
                remaining=len(self.plan.stages),
            )
            kwargs = self.kwargs
            kwargs.update({"flow_slug": self.flow.slug})
            return redirect_with_qs("authentik_api:flow-executor",
                                    self.request.GET, **kwargs)
        # User passed all stages
        self._logger.debug(
            "f(exec): User passed all stages",
            context=cleanse_dict(self.plan.context),
        )
        return self._flow_done()

    def stage_invalid(self,
                      error_message: Optional[str] = None) -> HttpResponse:
        """Callback used stage when data is correct but a policy denies access
        or the user account is disabled.

        Optionally, an exception can be passed, which will be shown if the current user
        is a superuser."""
        self._logger.debug("f(exec): Stage invalid")
        self.cancel()
        response = HttpChallengeResponse(
            AccessDeniedChallenge({
                "error_message": error_message,
                "title": self.flow.title,
                "type": ChallengeTypes.NATIVE.value,
                "component": "ak-stage-access-denied",
            }))
        return to_stage_response(self.request, response)

    def cancel(self):
        """Cancel current execution and return a redirect"""
        keys_to_delete = [
            SESSION_KEY_APPLICATION_PRE,
            SESSION_KEY_PLAN,
            SESSION_KEY_GET,
        ]
        for key in keys_to_delete:
            if key in self.request.session:
                del self.request.session[key]