flows: inspector (#1469)

* flows: add initial inspector

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: change naming a bit

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flow: add inspector frame

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: don't use shadydom when inspecting

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: add current stage to api

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/*: fix imports

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: deep-copy plan instead of just adding

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: ui

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: restrict inspector to admin

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: add buttons to launch flow with inspector

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: don't automatically follow redirects when inspector is open

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: make current_plan optional, only require historry

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: handle error messages in inspector

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: improve UI when flow is done

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: add is_completed flag to inspector

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: fix monkeypatches for tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: add inspector tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ci: re-enable cache

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-09-28 09:36:48 +02:00 committed by GitHub
parent ea4b920264
commit f9ad102915
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1009 additions and 170 deletions

View file

@ -25,14 +25,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run pylint - name: run pylint
run: pipenv run pylint authentik tests lifecycle run: pipenv run pylint authentik tests lifecycle
@ -43,14 +43,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run black - name: run black
run: pipenv run black --check authentik tests lifecycle run: pipenv run black --check authentik tests lifecycle
@ -61,14 +61,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run isort - name: run isort
run: pipenv run isort --check authentik tests lifecycle run: pipenv run isort --check authentik tests lifecycle
@ -79,14 +79,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run bandit - name: run bandit
run: pipenv run bandit -r authentik tests lifecycle run: pipenv run bandit -r authentik tests lifecycle
@ -113,14 +113,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run migrations - name: run migrations
run: pipenv run python -m lifecycle.migrate run: pipenv run python -m lifecycle.migrate
@ -138,14 +138,14 @@ jobs:
# Copy current, latest config to local # Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml cp authentik/lib/default.yml local.env.yml
git checkout $(git describe --abbrev=0 --match 'version/*') git checkout $(git describe --abbrev=0 --match 'version/*')
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- name: run migrations to stable - name: run migrations to stable
run: pipenv run python -m lifecycle.migrate run: pipenv run python -m lifecycle.migrate
@ -168,14 +168,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- uses: testspace-com/setup-testspace@v1 - uses: testspace-com/setup-testspace@v1
with: with:
@ -197,14 +197,14 @@ jobs:
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.9' python-version: '3.9'
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh run: scripts/ci_prepare.sh
- uses: testspace-com/setup-testspace@v1 - uses: testspace-com/setup-testspace@v1
with: with:
@ -236,14 +236,14 @@ jobs:
- uses: testspace-com/setup-testspace@v1 - uses: testspace-com/setup-testspace@v1
with: with:
domain: ${{github.repository_owner}} domain: ${{github.repository_owner}}
# - id: cache-pipenv - id: cache-pipenv
# uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
# with: with:
# path: ~/.local/share/virtualenvs path: ~/.local/share/virtualenvs
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare - name: prepare
# env: env:
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: | run: |
scripts/ci_prepare.sh scripts/ci_prepare.sh
docker-compose -f tests/e2e/ci.docker-compose.yml up -d docker-compose -f tests/e2e/ci.docker-compose.yml up -d

View file

@ -30,7 +30,8 @@ from authentik.events.api.notification_transport import NotificationTransportVie
from authentik.flows.api.bindings import FlowStageBindingViewSet from authentik.flows.api.bindings import FlowStageBindingViewSet
from authentik.flows.api.flows import FlowViewSet from authentik.flows.api.flows import FlowViewSet
from authentik.flows.api.stages import StageViewSet from authentik.flows.api.stages import StageViewSet
from authentik.flows.views import FlowExecutorView from authentik.flows.views.executor import FlowExecutorView
from authentik.flows.views.inspector import FlowInspectorView
from authentik.outposts.api.outposts import OutpostViewSet from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import ( from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet, DockerServiceConnectionViewSet,
@ -228,6 +229,11 @@ urlpatterns = (
FlowExecutorView.as_view(), FlowExecutorView.as_view(),
name="flow-executor", name="flow-executor",
), ),
path(
"flows/inspector/<slug:flow_slug>/",
FlowInspectorView.as_view(),
name="flow-inspector",
),
path("sentry/", SentryTunnelView.as_view(), name="sentry"), path("sentry/", SentryTunnelView.as_view(), name="sentry"),
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
] ]

View file

@ -8,7 +8,7 @@ from django.http.request import HttpRequest
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User
from authentik.events.utils import cleanse_dict, sanitize_dict from authentik.events.utils import cleanse_dict, sanitize_dict
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS

View file

@ -22,7 +22,7 @@ from authentik.flows.planner import (
PLAN_CONTEXT_SSO, PLAN_CONTEXT_SSO,
FlowPlanner, FlowPlanner,
) )
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_keys
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT

View file

@ -5,7 +5,7 @@
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
{% if flow.compatibility_mode %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script> <script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from authentik.core.auth import TokenBackend from authentik.core.auth import TokenBackend
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.tests.utils import get_request from authentik.lib.tests.utils import get_request

View file

@ -14,4 +14,5 @@ class FlowInterfaceView(TemplateView):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View file

@ -12,7 +12,7 @@ from authentik.core.signals import password_changed
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.tasks import event_notification_handler from authentik.events.tasks import event_notification_handler
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.invitation.models import Invitation from authentik.stages.invitation.models import Invitation
from authentik.stages.invitation.signals import invitation_used from authentik.stages.invitation.signals import invitation_used
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS

View file

@ -32,7 +32,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cach
from authentik.flows.transfer.common import DataclassEncoder from authentik.flows.transfer.common import DataclassEncoder
from authentik.flows.transfer.exporter import FlowExporter from authentik.flows.transfer.exporter import FlowExporter
from authentik.flows.transfer.importer import FlowImporter from authentik.flows.transfer.importer import FlowImporter
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
LOGGER = get_logger() LOGGER = get_logger()

View file

@ -18,7 +18,7 @@ from authentik.flows.challenge import (
) )
from authentik.flows.models import InvalidResponseAction from authentik.flows.models import InvalidResponseAction
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
from authentik.flows.views import FlowExecutorView from authentik.flows.views.executor import FlowExecutorView
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
LOGGER = get_logger() LOGGER = get_logger()

View file

@ -14,7 +14,7 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.flows.planner import FlowPlan, FlowPlanner from authentik.flows.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
@ -38,13 +38,13 @@ TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
class TestFlowExecutor(APITestCase): class TestFlowExecutor(APITestCase):
"""Test views logic""" """Test executor"""
def setUp(self): def setUp(self):
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_existing_plan_diff_flow(self): def test_existing_plan_diff_flow(self):
@ -62,7 +62,7 @@ class TestFlowExecutor(APITestCase):
session.save() session.save()
cancel_mock = MagicMock() cancel_mock = MagicMock()
with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", cancel_mock):
response = self.client.get( response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
) )
@ -70,7 +70,7 @@ class TestFlowExecutor(APITestCase):
self.assertEqual(cancel_mock.call_count, 2) self.assertEqual(cancel_mock.call_count, 2)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
@patch( @patch(
@ -105,7 +105,7 @@ class TestFlowExecutor(APITestCase):
) )
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_invalid_empty_flow(self): def test_invalid_empty_flow(self):
@ -124,7 +124,7 @@ class TestFlowExecutor(APITestCase):
self.assertEqual(response.url, reverse("authentik_core:root-redirect")) self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_invalid_flow_redirect(self): def test_invalid_flow_redirect(self):
@ -175,7 +175,7 @@ class TestFlowExecutor(APITestCase):
self.assertEqual(len(plan.bindings), 1) self.assertEqual(len(plan.bindings), 1)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_reevaluate_remove_last(self): def test_reevaluate_remove_last(self):

View file

@ -0,0 +1,92 @@
"""Flow inspector tests"""
from json import loads
from django.test.client import RequestFactory
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields
class TestFlowInspector(APITestCase):
"""Test inspector"""
def setUp(self):
self.request_factory = RequestFactory()
self.admin = User.objects.get(username="akadmin")
self.client.force_login(self.admin)
def test(self):
"""test inspector"""
flow = Flow.objects.create(
name="test-full",
slug="test-full",
designation=FlowDesignation.AUTHENTICATION,
)
# Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create(
name="ident",
user_fields=[UserFields.USERNAME],
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
)
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertJSONEqual(
res.content,
{
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"type": ChallengeTypes.NATIVE.value,
"password_fields": False,
"primary_action": "Log in",
"sources": [],
"user_fields": ["username"],
},
)
ins = self.client.get(
reverse("authentik_api:flow-inspector", kwargs={"flow_slug": flow.slug}),
)
content = loads(ins.content)
self.assertEqual(content["is_completed"], False)
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual(
content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
)
self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{"uid_field": "akadmin"},
follow=True,
)
ins = self.client.get(
reverse("authentik_api:flow-inspector", kwargs={"flow_slug": flow.slug}),
)
content = loads(ins.content)
self.assertEqual(content["is_completed"], False)
self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
self.assertEqual(
content["current_plan"]["plan_context"]["pending_user"]["username"], "akadmin"
)

View file

@ -4,7 +4,7 @@ from typing import Callable, Type
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views import FlowExecutorView from authentik.flows.views.executor import FlowExecutorView
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses

View file

@ -4,7 +4,7 @@ from django.urls import reverse
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
class TestHelperView(TestCase): class TestHelperView(TestCase):

View file

@ -2,7 +2,7 @@
from django.urls import path from django.urls import path
from authentik.flows.models import FlowDesignation from authentik.flows.models import FlowDesignation
from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow from authentik.flows.views.executor import CancelView, ConfigureFlowInitView, ToDefaultFlow
urlpatterns = [ urlpatterns = [
path( path(

View file

View file

@ -1,4 +1,5 @@
"""authentik multi-stage authentication engine""" """authentik multi-stage authentication engine"""
from copy import deepcopy
from traceback import format_tb from traceback import format_tb
from typing import Any, Optional from typing import Any, Optional
@ -52,6 +53,7 @@ NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN = "authentik_flows_plan" SESSION_KEY_PLAN = "authentik_flows_plan"
SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get" SESSION_KEY_GET = "authentik_flows_get"
SESSION_KEY_HISTORY = "authentik_flows_history"
def challenge_types(): def challenge_types():
@ -140,6 +142,7 @@ class FlowExecutorView(APIView):
# Don't check session again as we've either already loaded the plan or we need to plan # Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan: if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self._logger.debug("f(exec): No active Plan found, initiating planner") self._logger.debug("f(exec): No active Plan found, initiating planner")
try: try:
self.plan = self._initiate_plan() self.plan = self._initiate_plan()
@ -321,6 +324,7 @@ class FlowExecutorView(APIView):
"f(exec): Stage ok", "f(exec): Stage ok",
stage_class=class_to_path(self.current_stage_view.__class__), 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.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.bindings: if self.plan.bindings:
@ -368,6 +372,10 @@ class FlowExecutorView(APIView):
SESSION_KEY_APPLICATION_PRE, SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN, SESSION_KEY_PLAN,
SESSION_KEY_GET, 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: for key in keys_to_delete:
if key in self.request.session: if key in self.request.session:

View file

@ -0,0 +1,119 @@
"""Flow Inspector"""
from hashlib import sha256
from typing import Any
from django.conf import settings
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.fields import BooleanField, ListField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.api.utils import PassiveSerializer
from authentik.events.utils import sanitize_dict
from authentik.flows.api.bindings import FlowStageBindingSerializer
from authentik.flows.models import Flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
class FlowInspectorPlanSerializer(PassiveSerializer):
"""Serializer for an active FlowPlan"""
current_stage = SerializerMethodField()
next_planned_stage = SerializerMethodField(required=False)
plan_context = SerializerMethodField()
session_id = SerializerMethodField()
def get_current_stage(self, plan: FlowPlan) -> FlowStageBindingSerializer:
"""Get the current stage"""
return FlowStageBindingSerializer(instance=plan.bindings[0]).data
def get_next_planned_stage(self, plan: FlowPlan) -> FlowStageBindingSerializer:
"""Get the next planned stage"""
if len(plan.bindings) < 2:
return FlowStageBindingSerializer().data
return FlowStageBindingSerializer(instance=plan.bindings[1]).data
def get_plan_context(self, plan: FlowPlan) -> dict[str, Any]:
"""Get the plan's context, sanitized"""
return sanitize_dict(plan.context)
# pylint: disable=unused-argument
def get_session_id(self, plan: FlowPlan) -> str:
"""Get a unique session ID"""
request: Request = self.context["request"]
return sha256(
f"{request._request.session.session_key}-{settings.SECRET_KEY}".encode("ascii")
).hexdigest()
class FlowInspectionSerializer(PassiveSerializer):
"""Serializer for inspect endpoint"""
plans = ListField(child=FlowInspectorPlanSerializer())
current_plan = FlowInspectorPlanSerializer(required=False)
is_completed = BooleanField()
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowInspectorView(APIView):
"""Flow inspector API"""
permission_classes = [IsAdminUser]
flow: Flow
_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)
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
if SESSION_KEY_HISTORY not in self.request.session:
return HttpResponse(status=400)
return super().dispatch(request, flow_slug=flow_slug)
@extend_schema(
responses={
200: FlowInspectionSerializer(),
400: OpenApiResponse(
description="No flow plan in session."
), # This error can be raised by the email stage
},
request=OpenApiTypes.NONE,
operation_id="flows_inspector_get",
)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Get current flow state and record it"""
plans = []
for plan in request.session[SESSION_KEY_HISTORY]:
plan_serializer = FlowInspectorPlanSerializer(
instance=plan, context={"request": request}
)
plans.append(plan_serializer.data)
is_completed = False
if SESSION_KEY_PLAN in request.session:
current_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
else:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
is_completed = True
current_serializer = FlowInspectorPlanSerializer(
instance=current_plan, context={"request": request}
)
response = {
"plans": plans,
"current_plan": current_serializer.data,
"is_completed": is_completed,
}
return Response(response)

View file

@ -10,7 +10,7 @@ from django.views.generic.base import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User from authentik.core.models import Application, Provider, User
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine

View file

@ -23,7 +23,7 @@ from authentik.flows.planner import (
FlowPlanner, FlowPlanner,
) )
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message

View file

@ -13,7 +13,7 @@ from authentik.core.models import Application
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView from authentik.policies.views import PolicyAccessView

View file

@ -17,7 +17,7 @@ from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views import to_stage_response from authentik.flows.views.executor import to_stage_response
from authentik.sources.plex.models import PlexSource from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager

View file

@ -18,7 +18,7 @@ from authentik.flows.planner import (
PLAN_CONTEXT_SSO, PLAN_CONTEXT_SSO,
FlowPlanner, FlowPlanner,
) )
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_keys
from authentik.sources.saml.exceptions import ( from authentik.sources.saml.exceptions import (

View file

@ -22,7 +22,7 @@ from authentik.flows.planner import (
FlowPlanner, FlowPlanner,
) )
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64 from authentik.providers.saml.utils.encoding import nice64

View file

@ -12,7 +12,7 @@ from authentik.flows.challenge import (
) )
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views import InvalidStageError from authentik.flows.views.executor import InvalidStageError
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
LOGGER = get_logger() LOGGER = get_logger()

View file

@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.captcha.models import CaptchaStage from authentik.stages.captcha.models import CaptchaStage
# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do # https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do

View file

@ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent

View file

@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.deny.models import DenyStage from authentik.stages.deny.models import DenyStage

View file

@ -16,7 +16,7 @@ from authentik.core.models import Token
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views import SESSION_KEY_GET from authentik.flows.views.executor import SESSION_KEY_GET
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage

View file

@ -12,7 +12,7 @@ from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage

View file

@ -12,7 +12,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import QS_KEY_TOKEN from authentik.stages.email.stage import QS_KEY_TOKEN
@ -90,7 +90,7 @@ class TestEmailStage(APITestCase):
session.save() session.save()
token: Token = Token.objects.get(user=self.user) token: Token = Token.objects.get(user=self.user)
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session # Call the executor shell to preseed the session
url = reverse( url = reverse(
"authentik_api:flow-executor", "authentik_api:flow-executor",

View file

@ -18,7 +18,7 @@ from authentik.core.models import Application, Source, User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE, challenge_types from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, challenge_types
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate from authentik.stages.password.stage import authenticate

View file

@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
from authentik.flows.models import in_memory_stage 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.executor 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

View file

@ -12,8 +12,8 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.invitation.models import Invitation, InvitationStage from authentik.stages.invitation.models import Invitation, InvitationStage
from authentik.stages.invitation.stage import ( from authentik.stages.invitation.stage import (
INVITATION_TOKEN_KEY, INVITATION_TOKEN_KEY,
@ -40,7 +40,7 @@ class TestUserLoginStage(APITestCase):
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_without_invitation_fail(self): def test_without_invitation_fail(self):
@ -108,7 +108,7 @@ class TestUserLoginStage(APITestCase):
data = {"foo": "bar"} data = {"foo": "bar"}
invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data)
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex}) args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex})
response = self.client.get(base_url + f"?query={args}") response = self.client.get(base_url + f"?query={args}")
@ -140,7 +140,7 @@ class TestUserLoginStage(APITestCase):
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
session.save() session.save()
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
response = self.client.get(base_url, follow=True) response = self.client.get(base_url, follow=True)

View file

@ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_key from authentik.lib.generators import generate_key
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.models import PasswordStage from authentik.stages.password.models import PasswordStage
@ -39,7 +39,7 @@ class TestPasswordStage(APITestCase):
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_without_user(self): def test_without_user(self):
@ -153,7 +153,7 @@ class TestPasswordStage(APITestCase):
self.assertNotIn(SESSION_KEY_PLAN, self.client.session) self.assertNotIn(SESSION_KEY_PLAN, self.client.session)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
@patch( @patch(

View file

@ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse
@ -161,7 +161,7 @@ class TestPromptStage(APITestCase):
challenge_response = self.test_valid_challenge_with_policy() challenge_response = self.test_valid_challenge_with_policy()
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
response = self.client.post( response = self.client.post(
reverse( reverse(
"authentik_api:flow-executor", "authentik_api:flow-executor",

View file

@ -10,8 +10,8 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.user_delete.models import UserDeleteStage from authentik.stages.user_delete.models import UserDeleteStage
@ -32,7 +32,7 @@ class TestUserDeleteStage(APITestCase):
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_no_user(self): def test_no_user(self):

View file

@ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_login.models import UserLoginStage
@ -81,7 +81,7 @@ class TestUserLoginStage(APITestCase):
self.assertEqual(list(self.client.session.keys()), []) self.assertEqual(list(self.client.session.keys()), [])
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_without_user(self): def test_without_user(self):

View file

@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.user_logout.models import UserLogoutStage from authentik.stages.user_logout.models import UserLogoutStage

View file

@ -13,8 +13,8 @@ from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.models import UserWriteStage from authentik.stages.user_write.models import UserWriteStage
@ -112,7 +112,7 @@ class TestUserWriteStage(APITestCase):
self.assertNotIn("some_ignored_attribute", user_qs.first().attributes) self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_without_data(self): def test_without_data(self):
@ -142,7 +142,7 @@ class TestUserWriteStage(APITestCase):
) )
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_blank_username(self): def test_blank_username(self):
@ -177,7 +177,7 @@ class TestUserWriteStage(APITestCase):
) )
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_duplicate_data(self): def test_duplicate_data(self):

View file

@ -4743,6 +4743,31 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
/flows/inspector/{flow_slug}/:
get:
operationId: flows_inspector_get
description: Get current flow state and record it
parameters:
- in: path
name: flow_slug
schema:
type: string
required: true
tags:
- flows
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/FlowInspection'
description: ''
'400':
description: No flow plan in session.
'403':
$ref: '#/components/schemas/GenericError'
/flows/instances/: /flows/instances/:
get: get:
operationId: flows_instances_list operationId: flows_instances_list
@ -20273,6 +20298,45 @@ components:
readOnly: true readOnly: true
required: required:
- diagram - diagram
FlowInspection:
type: object
description: Serializer for inspect endpoint
properties:
plans:
type: array
items:
$ref: '#/components/schemas/FlowInspectorPlan'
current_plan:
$ref: '#/components/schemas/FlowInspectorPlan'
is_completed:
type: boolean
required:
- is_completed
- plans
FlowInspectorPlan:
type: object
description: Serializer for an active FlowPlan
properties:
current_stage:
allOf:
- $ref: '#/components/schemas/FlowStageBinding'
readOnly: true
next_planned_stage:
allOf:
- $ref: '#/components/schemas/FlowStageBinding'
readOnly: true
plan_context:
type: object
additionalProperties: {}
readOnly: true
session_id:
type: string
readOnly: true
required:
- current_stage
- next_planned_stage
- plan_context
- session_id
FlowRequest: FlowRequest:
type: object type: object
description: Flow Serializer description: Flow Serializer

View file

@ -14,6 +14,7 @@ export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle";
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh"; export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
export const EVENT_WS_MESSAGE = "ak-ws-message"; export const EVENT_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
export const WS_MSG_TYPE_MESSAGE = "message"; export const WS_MSG_TYPE_MESSAGE = "message";
export const WS_MSG_TYPE_REFRESH = "refresh"; export const WS_MSG_TYPE_REFRESH = "refresh";

View file

@ -11,10 +11,10 @@ export class Expand extends LitElement {
expanded = false; expanded = false;
@property() @property()
textOpen = "Show less"; textOpen = t`Show less`;
@property() @property()
textClosed = "Show more"; textClosed = t`Show more`;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFExpandableSection]; return [PFExpandableSection];

View file

@ -8,6 +8,7 @@ import { until } from "lit/directives/until";
import AKGlobal from "../authentik.css"; import AKGlobal from "../authentik.css";
import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css"; import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css"; import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFTitle from "@patternfly/patternfly/components/Title/title.css";
@ -26,12 +27,14 @@ import {
import { DEFAULT_CONFIG, tenant } from "../api/Config"; import { DEFAULT_CONFIG, tenant } from "../api/Config";
import { configureSentry } from "../api/Sentry"; import { configureSentry } from "../api/Sentry";
import { WebsocketClient } from "../common/ws"; import { WebsocketClient } from "../common/ws";
import { TITLE_DEFAULT } from "../constants"; import { EVENT_FLOW_ADVANCE, TITLE_DEFAULT } from "../constants";
import "../elements/LoadingOverlay"; import "../elements/LoadingOverlay";
import { DefaultTenant } from "../elements/sidebar/SidebarBrand"; import { DefaultTenant } from "../elements/sidebar/SidebarBrand";
import { first } from "../utils"; import { first } from "../utils";
import "./FlowInspector";
import "./access_denied/FlowAccessDenied"; import "./access_denied/FlowAccessDenied";
import "./sources/plex/PlexLoginInit"; import "./sources/plex/PlexLoginInit";
import "./stages/RedirectStage";
import "./stages/authenticator_duo/AuthenticatorDuoStage"; import "./stages/authenticator_duo/AuthenticatorDuoStage";
import "./stages/authenticator_static/AuthenticatorStaticStage"; import "./stages/authenticator_static/AuthenticatorStaticStage";
import "./stages/authenticator_totp/AuthenticatorTOTPStage"; import "./stages/authenticator_totp/AuthenticatorTOTPStage";
@ -59,7 +62,9 @@ export class FlowExecutor extends LitElement implements StageHost {
// Assign the location as soon as we get the challenge and *not* in the render function // Assign the location as soon as we get the challenge and *not* in the render function
// as the render function might be called multiple times, which will navigate multiple // as the render function might be called multiple times, which will navigate multiple
// times and can invalidate oauth codes // times and can invalidate oauth codes
if (value?.type === ChallengeChoices.Redirect) { // Also only auto-redirect when the inspector is open, so that a user can inspect the
// redirect in the inspector
if (value?.type === ChallengeChoices.Redirect && !this.inspectorOpen) {
console.debug( console.debug(
"authentik/flows: redirecting to url from server", "authentik/flows: redirecting to url from server",
(value as RedirectChallenge).to, (value as RedirectChallenge).to,
@ -86,10 +91,14 @@ export class FlowExecutor extends LitElement implements StageHost {
@property({ attribute: false }) @property({ attribute: false })
tenant?: CurrentTenant; tenant?: CurrentTenant;
@property({ attribute: false })
inspectorOpen: boolean;
ws: WebsocketClient; ws: WebsocketClient;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css` return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal]
.concat(css`
.ak-hidden { .ak-hidden {
display: none; display: none;
} }
@ -100,6 +109,9 @@ export class FlowExecutor extends LitElement implements StageHost {
font-family: monospace; font-family: monospace;
overflow-x: scroll; overflow-x: scroll;
} }
.pf-c-drawer__content {
background-color: transparent;
}
`); `);
} }
@ -107,6 +119,7 @@ export class FlowExecutor extends LitElement implements StageHost {
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.flowSlug = window.location.pathname.split("/")[3]; this.flowSlug = window.location.pathname.split("/")[3];
this.inspectorOpen = window.location.search.includes("inspector");
} }
setBackground(url: string): void { setBackground(url: string): void {
@ -130,6 +143,14 @@ export class FlowExecutor extends LitElement implements StageHost {
flowChallengeResponseRequest: payload, flowChallengeResponseRequest: payload,
}) })
.then((data) => { .then((data) => {
if (this.inspectorOpen) {
window.dispatchEvent(
new CustomEvent(EVENT_FLOW_ADVANCE, {
bubbles: true,
composed: true,
}),
);
}
this.challenge = data; this.challenge = data;
}) })
.catch((e: Error | Response) => { .catch((e: Error | Response) => {
@ -150,6 +171,14 @@ export class FlowExecutor extends LitElement implements StageHost {
query: window.location.search.substring(1), query: window.location.search.substring(1),
}) })
.then((challenge) => { .then((challenge) => {
if (this.inspectorOpen) {
window.dispatchEvent(
new CustomEvent(EVENT_FLOW_ADVANCE, {
bubbles: true,
composed: true,
}),
);
}
this.challenge = challenge; this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution // Only set background on first update, flow won't change throughout execution
if (this.challenge?.flowInfo?.background) { if (this.challenge?.flowInfo?.background) {
@ -199,6 +228,13 @@ export class FlowExecutor extends LitElement implements StageHost {
} }
switch (this.challenge.type) { switch (this.challenge.type) {
case ChallengeChoices.Redirect: case ChallengeChoices.Redirect:
if (this.inspectorOpen) {
return html`<ak-stage-redirect
.host=${this as StageHost}
.challenge=${this.challenge}
>
</ak-stage-redirect>`;
}
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
</ak-empty-state>`; </ak-empty-state>`;
case ChallengeChoices.Shell: case ChallengeChoices.Shell:
@ -333,50 +369,74 @@ export class FlowExecutor extends LitElement implements StageHost {
</filter> </filter>
</svg> </svg>
</div> </div>
<div class="pf-c-login"> <div class="pf-c-page__drawer">
<div class="ak-login-container"> <div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
<header class="pf-c-login__header"> <div class="pf-c-drawer__main">
<div class="pf-c-brand ak-brand"> <div class="pf-c-drawer__content">
<img <div class="pf-c-drawer__body">
src="${first( <div class="pf-c-login">
this.tenant?.brandingLogo, <div class="ak-login-container">
DefaultTenant.brandingLogo, <header class="pf-c-login__header">
)}" <div class="pf-c-brand ak-brand">
alt="authentik icon" <img
/> src="${first(
this.tenant?.brandingLogo,
DefaultTenant.brandingLogo,
)}"
alt="authentik icon"
/>
</div>
</header>
<div class="pf-c-login__main">
${this.renderChallengeWrapper()}
</div>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(
this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}"
>${link.name}</a
>
</li>`;
}),
)}
${this.tenant?.brandingTitle != "authentik"
? html`
<li>
<a href="https://goauthentik.io"
>${t`Powered by authentik`}</a
>
</li>
`
: html``}
${this.challenge?.flowInfo?.background?.startsWith(
"/static",
)
? html`
<li>
<a
href="https://unsplash.com/@introspectivedsgn"
>${t`Background image`}</a
>
</li>
`
: html``}
</ul>
</footer>
</div>
</div>
</div>
</div> </div>
</header>
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div> <ak-flow-inspector
<footer class="pf-c-login__footer"> class="pf-c-drawer__panel pf-m-width-33 ${this.inspectorOpen
<p></p> ? ""
<ul class="pf-c-list pf-m-inline"> : "display-none"}"
${until( ?hidden=${!this.inspectorOpen}
this.tenant?.uiFooterLinks?.map((link) => { ></ak-flow-inspector>
return html`<li> </div>
<a href="${link.href || ""}">${link.name}</a>
</li>`;
}),
)}
${this.tenant?.brandingTitle != "authentik"
? html`
<li>
<a href="https://goauthentik.io"
>${t`Powered by authentik`}</a
>
</li>
`
: html``}
${this.challenge?.flowInfo?.background?.startsWith("/static")
? html`
<li>
<a href="https://unsplash.com/@introspectivedsgn"
>${t`Background image`}</a
>
</li>
`
: html``}
</ul>
</footer>
</div> </div>
</div>`; </div>`;
} }

View file

@ -0,0 +1,297 @@
import { t } from "@lingui/macro";
import { css, CSSResult, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import AKGlobal from "../authentik.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFNotificationDrawer from "@patternfly/patternfly/components/NotificationDrawer/notification-drawer.css";
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FlowInspection, FlowsApi, Stage } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../api/Config";
import { EVENT_FLOW_ADVANCE } from "../constants";
import "../elements/Expand";
@customElement("ak-flow-inspector")
export class FlowInspector extends LitElement {
flowSlug: string;
@property({ attribute: false })
state?: FlowInspection;
@property({ attribute: false })
error?: Response;
static get styles(): CSSResult[] {
return [
PFBase,
PFStack,
PFCard,
PFNotificationDrawer,
PFDescriptionList,
PFProgressStepper,
AKGlobal,
css`
code.break {
word-break: break-all;
}
`,
];
}
constructor() {
super();
this.flowSlug = window.location.pathname.split("/")[3];
window.addEventListener(EVENT_FLOW_ADVANCE, this.advanceHandler as EventListener);
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_FLOW_ADVANCE, this.advanceHandler as EventListener);
}
advanceHandler = (): void => {
new FlowsApi(DEFAULT_CONFIG)
.flowsInspectorGet({
flowSlug: this.flowSlug,
})
.then((state) => {
this.state = state;
})
.catch((exc) => {
this.error = exc;
});
};
// getStage return a stage without flowSet, for brevity
getStage(stage?: Stage): unknown {
if (!stage) {
return stage;
}
delete stage.flowSet;
return stage;
}
renderAccessDenied(): TemplateResult {
return html`<div class="pf-c-drawer__body pf-m-no-padding">
<div class="pf-c-notification-drawer">
<div class="pf-c-notification-drawer__header">
<div class="text">
<h1 class="pf-c-notification-drawer__header-title">${t`Flow inspector`}</h1>
</div>
</div>
<div class="pf-c-notification-drawer__body">
<div class="pf-l-stack pf-m-gutter">
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__body">${this.error?.statusText}</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
}
render(): TemplateResult {
if (this.error) {
return this.renderAccessDenied();
}
if (!this.state) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<div class="pf-c-drawer__body pf-m-no-padding">
<div class="pf-c-notification-drawer">
<div class="pf-c-notification-drawer__header">
<div class="text">
<h1 class="pf-c-notification-drawer__header-title">${t`Flow inspector`}</h1>
</div>
</div>
<div class="pf-c-notification-drawer__body">
<div class="pf-l-stack pf-m-gutter">
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${t`Next stage`}</div>
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Stage name`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.state.currentPlan?.nextPlannedStage
?.stageObj?.name || "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Stage kind`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.state.currentPlan?.nextPlannedStage
?.stageObj?.verboseName || "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Stage object`}</span
>
</dt>
<dd class="pf-c-description-list__description">
${this.state.isCompleted
? html` <div
class="pf-c-description-list__text"
>
${t`This flow is completed.`}
</div>`
: html`<ak-expand>
<pre class="pf-c-description-list__text">
${JSON.stringify(this.getStage(this.state.currentPlan?.nextPlannedStage?.stageObj), null, 4)}</pre
>
</ak-expand>`}
</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${t`Plan history`}</div>
</div>
<div class="pf-c-card__body">
<ol class="pf-c-progress-stepper pf-m-vertical">
${this.state.plans.map((plan) => {
return html`<li
class="pf-c-progress-stepper__step pf-m-success"
>
<div class="pf-c-progress-stepper__step-connector">
<span class="pf-c-progress-stepper__step-icon">
<i
class="fas fa-check-circle"
aria-hidden="true"
></i>
</span>
</div>
<div class="pf-c-progress-stepper__step-main">
<div class="pf-c-progress-stepper__step-title">
${plan.currentStage.stageObj?.name}
</div>
<div
class="pf-c-progress-stepper__step-description"
>
${plan.currentStage.stageObj?.verboseName}
</div>
</div>
</li> `;
})}
${this.state.currentPlan?.currentStage &&
!this.state.isCompleted
? html` <li
class="pf-c-progress-stepper__step pf-m-current pf-m-info"
>
<div
class="pf-c-progress-stepper__step-connector"
>
<span
class="pf-c-progress-stepper__step-icon"
>
<i
class="pficon pf-icon-resources-full"
aria-hidden="true"
></i>
</span>
</div>
<div class="pf-c-progress-stepper__step-main">
<div
class="pf-c-progress-stepper__step-title"
>
${this.state.currentPlan?.currentStage
?.stageObj?.name}
</div>
<div
class="pf-c-progress-stepper__step-description"
>
${this.state.currentPlan?.currentStage
?.stageObj?.verboseName}
</div>
</div>
</li>`
: html``}
${this.state.currentPlan?.nextPlannedStage &&
!this.state.isCompleted
? html`<li
class="pf-c-progress-stepper__step pf-m-pending"
>
<div
class="pf-c-progress-stepper__step-connector"
>
<span
class="pf-c-progress-stepper__step-icon"
></span>
</div>
<div class="pf-c-progress-stepper__step-main">
<div
class="pf-c-progress-stepper__step-title"
>
${this.state.currentPlan.nextPlannedStage
.stageObj?.name}
</div>
<div
class="pf-c-progress-stepper__step-description"
>
${this.state.currentPlan?.nextPlannedStage
?.stageObj?.verboseName}
</div>
</div>
</li>`
: html``}
</ol>
</div>
</div>
</div>
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${t`Current plan cntext`}</div>
</div>
<div class="pf-c-card__body">
<pre>
${JSON.stringify(this.state.currentPlan?.planContext, null, 4)}</pre
>
</div>
</div>
</div>
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${t`Session ID`}</div>
</div>
<div class="pf-c-card__body">
<code class="break">${this.state.currentPlan?.sessionId}</code>
</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
}
}

View file

@ -0,0 +1,56 @@
import { t } from "@lingui/macro";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import AKGlobal from "../../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FlowChallengeResponseRequest, RedirectChallenge } from "@goauthentik/api";
import { BaseStage } from "./base";
@customElement("ak-stage-redirect")
export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFButton, PFFormControl, PFTitle, AKGlobal];
}
renderURL(): string {
if (!this.challenge.to.includes("://")) {
return window.location.origin + this.challenge.to;
}
return this.challenge.to;
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${t`Redirect`}</h1>
</header>
<div class="pf-c-login__main-body">
<form method="POST" class="pf-c-form">
<div class="pf-c-form__group">
<p>${t`You're about to be redirect to the following URL.`}</p>
<pre>${this.renderURL()}</pre>
</div>
<div class="pf-c-form__group pf-m-action">
<a
type="submit"
class="pf-c-button pf-m-primary pf-m-block"
href=${this.challenge.to}
>
${t`Follow redirect`}
</a>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer> `;
}
}

View file

@ -1151,6 +1151,10 @@ msgstr "Created {0}"
msgid "Creation Date" msgid "Creation Date"
msgstr "Creation Date" msgstr "Creation Date"
#: src/flows/FlowInspector.ts
msgid "Current plan cntext"
msgstr "Current plan cntext"
#: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationForm.ts
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
msgid "Currently set to:" msgid "Currently set to:"
@ -1626,6 +1630,10 @@ msgstr "Execute"
msgid "Execute flow" msgid "Execute flow"
msgstr "Execute flow" msgstr "Execute flow"
#: src/pages/flows/FlowViewPage.ts
msgid "Execute with inspector"
msgstr "Execute with inspector"
#: src/pages/policies/expression/ExpressionPolicyForm.ts #: src/pages/policies/expression/ExpressionPolicyForm.ts
msgid "Executes the python snippet to determine whether to allow or deny a request." msgid "Executes the python snippet to determine whether to allow or deny a request."
msgstr "Executes the python snippet to determine whether to allow or deny a request." msgstr "Executes the python snippet to determine whether to allow or deny a request."
@ -1793,6 +1801,11 @@ msgstr "Flow"
msgid "Flow Overview" msgid "Flow Overview"
msgstr "Flow Overview" msgstr "Flow Overview"
#: src/flows/FlowInspector.ts
#: src/flows/FlowInspector.ts
msgid "Flow inspector"
msgstr "Flow inspector"
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts #: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts #: src/pages/sources/saml/SAMLSourceForm.ts
@ -1860,6 +1873,10 @@ msgstr "Flows"
msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them."
msgstr "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." msgstr "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them."
#: src/flows/stages/RedirectStage.ts
msgid "Follow redirect"
msgstr "Follow redirect"
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Force the user to configure an authenticator" msgid "Force the user to configure an authenticator"
msgstr "Force the user to configure an authenticator" msgstr "Force the user to configure an authenticator"
@ -2350,6 +2367,7 @@ msgstr "Load servers"
#: src/elements/table/Table.ts #: src/elements/table/Table.ts
#: src/flows/FlowExecutor.ts #: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts #: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/access_denied/FlowAccessDenied.ts #: src/flows/access_denied/FlowAccessDenied.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
@ -2714,6 +2732,10 @@ msgstr "New version available!"
msgid "Newly created users are added to this group, if a group is selected." msgid "Newly created users are added to this group, if a group is selected."
msgstr "Newly created users are added to this group, if a group is selected." msgstr "Newly created users are added to this group, if a group is selected."
#: src/flows/FlowInspector.ts
msgid "Next stage"
msgstr "Next stage"
#: src/elements/oauth/UserRefreshList.ts #: src/elements/oauth/UserRefreshList.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
@ -3079,6 +3101,10 @@ msgstr "Persistent"
msgid "Placeholder" msgid "Placeholder"
msgstr "Placeholder" msgstr "Placeholder"
#: src/flows/FlowInspector.ts
msgid "Plan history"
msgstr "Plan history"
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
msgid "Please enter your TOTP Code" msgid "Please enter your TOTP Code"
@ -3381,6 +3407,7 @@ msgstr "Recovery keys"
msgid "Recovery link cannot be emailed, user has no email address saved." msgid "Recovery link cannot be emailed, user has no email address saved."
msgstr "Recovery link cannot be emailed, user has no email address saved." msgstr "Recovery link cannot be emailed, user has no email address saved."
#: src/flows/stages/RedirectStage.ts
#: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts
msgid "Redirect" msgid "Redirect"
msgstr "Redirect" msgstr "Redirect"
@ -3748,6 +3775,10 @@ msgstr "Service Provider Binding"
#~ msgid "Session" #~ msgid "Session"
#~ msgstr "Session" #~ msgstr "Session"
#: src/flows/FlowInspector.ts
msgid "Session ID"
msgstr "Session ID"
#: src/pages/stages/user_login/UserLoginStageForm.ts #: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Session duration" msgid "Session duration"
msgstr "Session duration" msgstr "Session duration"
@ -3794,10 +3825,18 @@ msgstr "Severity"
msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable."
msgstr "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." msgstr "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable."
#: src/elements/Expand.ts
msgid "Show less"
msgstr "Show less"
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Show matched user" msgid "Show matched user"
msgstr "Show matched user" msgstr "Show matched user"
#: src/elements/Expand.ts
msgid "Show more"
msgstr "Show more"
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
msgid "Shown as the Title in Flow pages." msgid "Shown as the Title in Flow pages."
msgstr "Shown as the Title in Flow pages." msgstr "Shown as the Title in Flow pages."
@ -3897,6 +3936,18 @@ msgstr "Stage Configuration"
msgid "Stage binding(s)" msgid "Stage binding(s)"
msgstr "Stage binding(s)" msgstr "Stage binding(s)"
#: src/flows/FlowInspector.ts
msgid "Stage kind"
msgstr "Stage kind"
#: src/flows/FlowInspector.ts
msgid "Stage name"
msgstr "Stage name"
#: src/flows/FlowInspector.ts
msgid "Stage object"
msgstr "Stage object"
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
msgid "Stage type" msgid "Stage type"
msgstr "Stage type" msgstr "Stage type"
@ -4505,6 +4556,10 @@ msgstr ""
msgid "These policies control which users can access this application." msgid "These policies control which users can access this application."
msgstr "These policies control which users can access this application." msgstr "These policies control which users can access this application."
#: src/flows/FlowInspector.ts
msgid "This flow is completed."
msgstr "This flow is completed."
#: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
@ -5276,6 +5331,10 @@ msgstr "Yes"
msgid "You can only select providers that match the type of the outpost." msgid "You can only select providers that match the type of the outpost."
msgstr "You can only select providers that match the type of the outpost." msgstr "You can only select providers that match the type of the outpost."
#: src/flows/stages/RedirectStage.ts
msgid "You're about to be redirect to the following URL."
msgstr "You're about to be redirect to the following URL."
#: src/interfaces/AdminInterface.ts #: src/interfaces/AdminInterface.ts
msgid "You're currently impersonating {0}. Click to stop." msgid "You're currently impersonating {0}. Click to stop."
msgstr "You're currently impersonating {0}. Click to stop." msgstr "You're currently impersonating {0}. Click to stop."

View file

@ -1145,6 +1145,10 @@ msgstr ""
msgid "Creation Date" msgid "Creation Date"
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
msgid "Current plan cntext"
msgstr ""
#: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationForm.ts
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
msgid "Currently set to:" msgid "Currently set to:"
@ -1618,6 +1622,10 @@ msgstr ""
msgid "Execute flow" msgid "Execute flow"
msgstr "" msgstr ""
#: src/pages/flows/FlowViewPage.ts
msgid "Execute with inspector"
msgstr ""
#: src/pages/policies/expression/ExpressionPolicyForm.ts #: src/pages/policies/expression/ExpressionPolicyForm.ts
msgid "Executes the python snippet to determine whether to allow or deny a request." msgid "Executes the python snippet to determine whether to allow or deny a request."
msgstr "" msgstr ""
@ -1785,6 +1793,11 @@ msgstr ""
msgid "Flow Overview" msgid "Flow Overview"
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
#: src/flows/FlowInspector.ts
msgid "Flow inspector"
msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts #: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts #: src/pages/sources/saml/SAMLSourceForm.ts
@ -1852,6 +1865,10 @@ msgstr ""
msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them."
msgstr "" msgstr ""
#: src/flows/stages/RedirectStage.ts
msgid "Follow redirect"
msgstr ""
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Force the user to configure an authenticator" msgid "Force the user to configure an authenticator"
msgstr "" msgstr ""
@ -2342,6 +2359,7 @@ msgstr ""
#: src/elements/table/Table.ts #: src/elements/table/Table.ts
#: src/flows/FlowExecutor.ts #: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts #: src/flows/FlowExecutor.ts
#: src/flows/FlowInspector.ts
#: src/flows/access_denied/FlowAccessDenied.ts #: src/flows/access_denied/FlowAccessDenied.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
@ -2706,6 +2724,10 @@ msgstr ""
msgid "Newly created users are added to this group, if a group is selected." msgid "Newly created users are added to this group, if a group is selected."
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
msgid "Next stage"
msgstr ""
#: src/elements/oauth/UserRefreshList.ts #: src/elements/oauth/UserRefreshList.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
@ -3071,6 +3093,10 @@ msgstr ""
msgid "Placeholder" msgid "Placeholder"
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
msgid "Plan history"
msgstr ""
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
msgid "Please enter your TOTP Code" msgid "Please enter your TOTP Code"
@ -3373,6 +3399,7 @@ msgstr ""
msgid "Recovery link cannot be emailed, user has no email address saved." msgid "Recovery link cannot be emailed, user has no email address saved."
msgstr "" msgstr ""
#: src/flows/stages/RedirectStage.ts
#: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts
msgid "Redirect" msgid "Redirect"
msgstr "" msgstr ""
@ -3740,6 +3767,10 @@ msgstr ""
#~ msgid "Session" #~ msgid "Session"
#~ msgstr "" #~ msgstr ""
#: src/flows/FlowInspector.ts
msgid "Session ID"
msgstr ""
#: src/pages/stages/user_login/UserLoginStageForm.ts #: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Session duration" msgid "Session duration"
msgstr "" msgstr ""
@ -3786,10 +3817,18 @@ msgstr ""
msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable."
msgstr "" msgstr ""
#: src/elements/Expand.ts
msgid "Show less"
msgstr ""
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Show matched user" msgid "Show matched user"
msgstr "" msgstr ""
#: src/elements/Expand.ts
msgid "Show more"
msgstr ""
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
msgid "Shown as the Title in Flow pages." msgid "Shown as the Title in Flow pages."
msgstr "" msgstr ""
@ -3889,6 +3928,18 @@ msgstr ""
msgid "Stage binding(s)" msgid "Stage binding(s)"
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
msgid "Stage kind"
msgstr ""
#: src/flows/FlowInspector.ts
msgid "Stage name"
msgstr ""
#: src/flows/FlowInspector.ts
msgid "Stage object"
msgstr ""
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
msgid "Stage type" msgid "Stage type"
msgstr "" msgstr ""
@ -4490,6 +4541,10 @@ msgstr ""
msgid "These policies control which users can access this application." msgid "These policies control which users can access this application."
msgstr "" msgstr ""
#: src/flows/FlowInspector.ts
msgid "This flow is completed."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
msgstr "" msgstr ""
@ -5259,6 +5314,10 @@ msgstr ""
msgid "You can only select providers that match the type of the outpost." msgid "You can only select providers that match the type of the outpost."
msgstr "" msgstr ""
#: src/flows/stages/RedirectStage.ts
msgid "You're about to be redirect to the following URL."
msgstr ""
#: src/interfaces/AdminInterface.ts #: src/interfaces/AdminInterface.ts
msgid "You're currently impersonating {0}. Click to stop." msgid "You're currently impersonating {0}. Click to stop."
msgstr "" msgstr ""

View file

@ -104,7 +104,9 @@ export class FlowListPage extends TablePage<Flow> {
slug: item.slug, slug: item.slug,
}) })
.then((link) => { .then((link) => {
window.open(`${link.link}?next=/%23${window.location.href}`); window.open(
`${link.link}?inspector&next=/%23${window.location.href}`,
);
}); });
}} }}
> >

View file

@ -107,6 +107,21 @@ export class FlowViewPage extends LitElement {
> >
${t`Execute`} ${t`Execute`}
</button> </button>
<button
class="pf-c-button pf-m-secondary"
@click=${() => {
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesExecuteRetrieve({
slug: this.flow.slug,
})
.then((link) => {
const finalURL = `${link.link}?inspector&next=/%23${window.location.hash}`;
window.open(finalURL, "_blank");
});
}}
>
${t`Execute with inspector`}
</button>
</div> </div>
</dd> </dd>
<dt class="pf-c-description-list__term"> <dt class="pf-c-description-list__term">