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:
parent
ea4b920264
commit
f9ad102915
126
.github/workflows/ci-main.yml
vendored
126
.github/workflows/ci-main.yml
vendored
|
@ -25,14 +25,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run pylint
|
||||
run: pipenv run pylint authentik tests lifecycle
|
||||
|
@ -43,14 +43,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run black
|
||||
run: pipenv run black --check authentik tests lifecycle
|
||||
|
@ -61,14 +61,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run isort
|
||||
run: pipenv run isort --check authentik tests lifecycle
|
||||
|
@ -79,14 +79,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run bandit
|
||||
run: pipenv run bandit -r authentik tests lifecycle
|
||||
|
@ -113,14 +113,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run migrations
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
|
@ -138,14 +138,14 @@ jobs:
|
|||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run migrations to stable
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
|
@ -168,14 +168,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
|
@ -197,14 +197,14 @@ jobs:
|
|||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
|
@ -236,14 +236,14 @@ jobs:
|
|||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
# - id: cache-pipenv
|
||||
# uses: actions/cache@v2.1.6
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
# env:
|
||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
docker-compose -f tests/e2e/ci.docker-compose.yml up -d
|
||||
|
|
|
@ -30,7 +30,8 @@ from authentik.events.api.notification_transport import NotificationTransportVie
|
|||
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
||||
from authentik.flows.api.flows import FlowViewSet
|
||||
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.service_connections import (
|
||||
DockerServiceConnectionViewSet,
|
||||
|
@ -228,6 +229,11 @@ urlpatterns = (
|
|||
FlowExecutorView.as_view(),
|
||||
name="flow-executor",
|
||||
),
|
||||
path(
|
||||
"flows/inspector/<slug:flow_slug>/",
|
||||
FlowInspectorView.as_view(),
|
||||
name="flow-inspector",
|
||||
),
|
||||
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
|
||||
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||
]
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.http.request import HttpRequest
|
|||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from authentik.flows.planner import (
|
|||
PLAN_CONTEXT_SSO,
|
||||
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.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block head_before %}
|
||||
{{ block.super }}
|
||||
{% if flow.compatibility_mode %}
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.test import TestCase
|
|||
from authentik.core.auth import TokenBackend
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -14,4 +14,5 @@ class FlowInterfaceView(TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
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)
|
||||
|
|
|
@ -12,7 +12,7 @@ from authentik.core.signals import password_changed
|
|||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.tasks import event_notification_handler
|
||||
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.signals import invitation_used
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
|
|
@ -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.exporter import FlowExporter
|
||||
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
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
|
|
@ -18,7 +18,7 @@ from authentik.flows.challenge import (
|
|||
)
|
||||
from authentik.flows.models import InvalidResponseAction
|
||||
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"
|
||||
LOGGER = get_logger()
|
||||
|
|
|
@ -14,7 +14,7 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker
|
|||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
|
||||
from authentik.flows.planner import FlowPlan, FlowPlanner
|
||||
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.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
|
@ -38,13 +38,13 @@ TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
|||
|
||||
|
||||
class TestFlowExecutor(APITestCase):
|
||||
"""Test views logic"""
|
||||
"""Test executor"""
|
||||
|
||||
def setUp(self):
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_existing_plan_diff_flow(self):
|
||||
|
@ -62,7 +62,7 @@ class TestFlowExecutor(APITestCase):
|
|||
session.save()
|
||||
|
||||
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(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
|
@ -70,7 +70,7 @@ class TestFlowExecutor(APITestCase):
|
|||
self.assertEqual(cancel_mock.call_count, 2)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
@patch(
|
||||
|
@ -105,7 +105,7 @@ class TestFlowExecutor(APITestCase):
|
|||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_invalid_empty_flow(self):
|
||||
|
@ -124,7 +124,7 @@ class TestFlowExecutor(APITestCase):
|
|||
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_invalid_flow_redirect(self):
|
||||
|
@ -175,7 +175,7 @@ class TestFlowExecutor(APITestCase):
|
|||
self.assertEqual(len(plan.bindings), 1)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_reevaluate_remove_last(self):
|
92
authentik/flows/tests/test_inspector.py
Normal file
92
authentik/flows/tests/test_inspector.py
Normal 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"
|
||||
)
|
|
@ -4,7 +4,7 @@ from typing import Callable, Type
|
|||
from django.test import RequestFactory, TestCase
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.urls import reverse
|
|||
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
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):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from django.urls import path
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow
|
||||
from authentik.flows.views.executor import CancelView, ConfigureFlowInitView, ToDefaultFlow
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
|
|
0
authentik/flows/views/__init__.py
Normal file
0
authentik/flows/views/__init__.py
Normal file
|
@ -1,4 +1,5 @@
|
|||
"""authentik multi-stage authentication engine"""
|
||||
from copy import deepcopy
|
||||
from traceback import format_tb
|
||||
from typing import Any, Optional
|
||||
|
||||
|
@ -52,6 +53,7 @@ NEXT_ARG_NAME = "next"
|
|||
SESSION_KEY_PLAN = "authentik_flows_plan"
|
||||
SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
|
||||
SESSION_KEY_GET = "authentik_flows_get"
|
||||
SESSION_KEY_HISTORY = "authentik_flows_history"
|
||||
|
||||
|
||||
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
|
||||
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()
|
||||
|
@ -321,6 +324,7 @@ class FlowExecutorView(APIView):
|
|||
"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:
|
||||
|
@ -368,6 +372,10 @@ class FlowExecutorView(APIView):
|
|||
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:
|
119
authentik/flows/views/inspector.py
Normal file
119
authentik/flows/views/inspector.py
Normal 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)
|
|
@ -10,7 +10,7 @@ from django.views.generic.base import View
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
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.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
|
|
@ -23,7 +23,7 @@ from authentik.flows.planner import (
|
|||
FlowPlanner,
|
||||
)
|
||||
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.urls import redirect_with_qs
|
||||
from authentik.lib.views import bad_request_message
|
||||
|
|
|
@ -13,7 +13,7 @@ from authentik.core.models import Application
|
|||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import in_memory_stage
|
||||
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.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
|
|
@ -17,7 +17,7 @@ from authentik.core.api.sources import SourceSerializer
|
|||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
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.plex import PlexAuth, PlexSourceFlowManager
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from authentik.flows.planner import (
|
|||
PLAN_CONTEXT_SSO,
|
||||
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.policies.utils import delete_none_keys
|
||||
from authentik.sources.saml.exceptions import (
|
||||
|
|
|
@ -22,7 +22,7 @@ from authentik.flows.planner import (
|
|||
FlowPlanner,
|
||||
)
|
||||
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.views import bad_request_message
|
||||
from authentik.providers.saml.utils.encoding import nice64
|
||||
|
|
|
@ -12,7 +12,7 @@ from authentik.flows.challenge import (
|
|||
)
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
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
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
|
|
@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
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
|
||||
|
||||
# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
|
||||
|
|
|
@ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from authentik.core.models import Token
|
|||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
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.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
|
|
@ -12,7 +12,7 @@ from authentik.events.models import Event, EventAction
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.stage import QS_KEY_TOKEN
|
||||
|
||||
|
@ -90,7 +90,7 @@ class TestEmailStage(APITestCase):
|
|||
session.save()
|
||||
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
|
||||
url = reverse(
|
||||
"authentik_api:flow-executor",
|
||||
|
|
|
@ -18,7 +18,7 @@ from authentik.core.models import Application, Source, User
|
|||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
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.signals import identification_failed
|
||||
from authentik.stages.password.stage import authenticate
|
||||
|
|
|
@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.flows.models import in_memory_stage
|
||||
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.signals import invitation_used
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
|
|
@ -12,8 +12,8 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
from authentik.stages.invitation.stage import (
|
||||
INVITATION_TOKEN_KEY,
|
||||
|
@ -40,7 +40,7 @@ class TestUserLoginStage(APITestCase):
|
|||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_without_invitation_fail(self):
|
||||
|
@ -108,7 +108,7 @@ class TestUserLoginStage(APITestCase):
|
|||
data = {"foo": "bar"}
|
||||
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})
|
||||
args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex})
|
||||
response = self.client.get(base_url + f"?query={args}")
|
||||
|
@ -140,7 +140,7 @@ class TestUserLoginStage(APITestCase):
|
|||
session[SESSION_KEY_PLAN] = plan
|
||||
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})
|
||||
response = self.client.get(base_url, follow=True)
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
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)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_without_user(self):
|
||||
|
@ -153,7 +153,7 @@ class TestPasswordStage(APITestCase):
|
|||
self.assertNotIn(SESSION_KEY_PLAN, self.client.session)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
@patch(
|
||||
|
|
|
@ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
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.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
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()
|
||||
|
||||
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
|
||||
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:flow-executor",
|
||||
|
|
|
@ -10,8 +10,8 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
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)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_no_user(self):
|
||||
|
|
|
@ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
|
||||
|
@ -81,7 +81,7 @@ class TestUserLoginStage(APITestCase):
|
|||
self.assertEqual(list(self.client.session.keys()), [])
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_without_user(self):
|
||||
|
|
|
@ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.user_logout.models import UserLogoutStage
|
||||
|
|
|
@ -13,8 +13,8 @@ from authentik.flows.challenge import ChallengeTypes
|
|||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
@ -112,7 +112,7 @@ class TestUserWriteStage(APITestCase):
|
|||
self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_without_data(self):
|
||||
|
@ -142,7 +142,7 @@ class TestUserWriteStage(APITestCase):
|
|||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_blank_username(self):
|
||||
|
@ -177,7 +177,7 @@ class TestUserWriteStage(APITestCase):
|
|||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_duplicate_data(self):
|
||||
|
|
64
schema.yml
64
schema.yml
|
@ -4743,6 +4743,31 @@ paths:
|
|||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$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/:
|
||||
get:
|
||||
operationId: flows_instances_list
|
||||
|
@ -20273,6 +20298,45 @@ components:
|
|||
readOnly: true
|
||||
required:
|
||||
- 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:
|
||||
type: object
|
||||
description: Flow Serializer
|
||||
|
|
|
@ -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_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
|
||||
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_REFRESH = "refresh";
|
||||
|
|
|
@ -11,10 +11,10 @@ export class Expand extends LitElement {
|
|||
expanded = false;
|
||||
|
||||
@property()
|
||||
textOpen = "Show less";
|
||||
textOpen = t`Show less`;
|
||||
|
||||
@property()
|
||||
textClosed = "Show more";
|
||||
textClosed = t`Show more`;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFExpandableSection];
|
||||
|
|
|
@ -8,6 +8,7 @@ import { until } from "lit/directives/until";
|
|||
import AKGlobal from "../authentik.css";
|
||||
import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.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 PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
|
@ -26,12 +27,14 @@ import {
|
|||
import { DEFAULT_CONFIG, tenant } from "../api/Config";
|
||||
import { configureSentry } from "../api/Sentry";
|
||||
import { WebsocketClient } from "../common/ws";
|
||||
import { TITLE_DEFAULT } from "../constants";
|
||||
import { EVENT_FLOW_ADVANCE, TITLE_DEFAULT } from "../constants";
|
||||
import "../elements/LoadingOverlay";
|
||||
import { DefaultTenant } from "../elements/sidebar/SidebarBrand";
|
||||
import { first } from "../utils";
|
||||
import "./FlowInspector";
|
||||
import "./access_denied/FlowAccessDenied";
|
||||
import "./sources/plex/PlexLoginInit";
|
||||
import "./stages/RedirectStage";
|
||||
import "./stages/authenticator_duo/AuthenticatorDuoStage";
|
||||
import "./stages/authenticator_static/AuthenticatorStaticStage";
|
||||
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
|
||||
// as the render function might be called multiple times, which will navigate multiple
|
||||
// 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(
|
||||
"authentik/flows: redirecting to url from server",
|
||||
(value as RedirectChallenge).to,
|
||||
|
@ -86,10 +91,14 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
@property({ attribute: false })
|
||||
tenant?: CurrentTenant;
|
||||
|
||||
@property({ attribute: false })
|
||||
inspectorOpen: boolean;
|
||||
|
||||
ws: WebsocketClient;
|
||||
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
@ -100,6 +109,9 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
font-family: monospace;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.pf-c-drawer__content {
|
||||
background-color: transparent;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
|
@ -107,6 +119,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
this.flowSlug = window.location.pathname.split("/")[3];
|
||||
this.inspectorOpen = window.location.search.includes("inspector");
|
||||
}
|
||||
|
||||
setBackground(url: string): void {
|
||||
|
@ -130,6 +143,14 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
flowChallengeResponseRequest: payload,
|
||||
})
|
||||
.then((data) => {
|
||||
if (this.inspectorOpen) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_FLOW_ADVANCE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.challenge = data;
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
|
@ -150,6 +171,14 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
query: window.location.search.substring(1),
|
||||
})
|
||||
.then((challenge) => {
|
||||
if (this.inspectorOpen) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_FLOW_ADVANCE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.challenge = challenge;
|
||||
// Only set background on first update, flow won't change throughout execution
|
||||
if (this.challenge?.flowInfo?.background) {
|
||||
|
@ -199,6 +228,13 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
}
|
||||
switch (this.challenge.type) {
|
||||
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`}>
|
||||
</ak-empty-state>`;
|
||||
case ChallengeChoices.Shell:
|
||||
|
@ -333,6 +369,11 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<div class="pf-c-login">
|
||||
<div class="ak-login-container">
|
||||
<header class="pf-c-login__header">
|
||||
|
@ -346,14 +387,18 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
|
||||
<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>
|
||||
<a href="${link.href || ""}"
|
||||
>${link.name}</a
|
||||
>
|
||||
</li>`;
|
||||
}),
|
||||
)}
|
||||
|
@ -366,10 +411,13 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
</li>
|
||||
`
|
||||
: html``}
|
||||
${this.challenge?.flowInfo?.background?.startsWith("/static")
|
||||
${this.challenge?.flowInfo?.background?.startsWith(
|
||||
"/static",
|
||||
)
|
||||
? html`
|
||||
<li>
|
||||
<a href="https://unsplash.com/@introspectivedsgn"
|
||||
<a
|
||||
href="https://unsplash.com/@introspectivedsgn"
|
||||
>${t`Background image`}</a
|
||||
>
|
||||
</li>
|
||||
|
@ -378,6 +426,18 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ak-flow-inspector
|
||||
class="pf-c-drawer__panel pf-m-width-33 ${this.inspectorOpen
|
||||
? ""
|
||||
: "display-none"}"
|
||||
?hidden=${!this.inspectorOpen}
|
||||
></ak-flow-inspector>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
297
web/src/flows/FlowInspector.ts
Normal file
297
web/src/flows/FlowInspector.ts
Normal 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>`;
|
||||
}
|
||||
}
|
56
web/src/flows/stages/RedirectStage.ts
Normal file
56
web/src/flows/stages/RedirectStage.ts
Normal 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> `;
|
||||
}
|
||||
}
|
|
@ -1151,6 +1151,10 @@ msgstr "Created {0}"
|
|||
msgid "Creation Date"
|
||||
msgstr "Creation Date"
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Current plan cntext"
|
||||
msgstr "Current plan cntext"
|
||||
|
||||
#: src/pages/applications/ApplicationForm.ts
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
msgid "Currently set to:"
|
||||
|
@ -1626,6 +1630,10 @@ msgstr "Execute"
|
|||
msgid "Execute flow"
|
||||
msgstr "Execute flow"
|
||||
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Execute with inspector"
|
||||
msgstr "Execute with inspector"
|
||||
|
||||
#: src/pages/policies/expression/ExpressionPolicyForm.ts
|
||||
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."
|
||||
|
@ -1793,6 +1801,11 @@ msgstr "Flow"
|
|||
msgid "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/plex/PlexSourceForm.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."
|
||||
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
|
||||
msgid "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/flows/FlowExecutor.ts
|
||||
#: src/flows/FlowExecutor.ts
|
||||
#: src/flows/FlowInspector.ts
|
||||
#: src/flows/access_denied/FlowAccessDenied.ts
|
||||
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.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."
|
||||
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/pages/applications/ApplicationCheckAccessForm.ts
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
|
@ -3079,6 +3101,10 @@ msgstr "Persistent"
|
|||
msgid "Placeholder"
|
||||
msgstr "Placeholder"
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Plan history"
|
||||
msgstr "Plan history"
|
||||
|
||||
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
|
||||
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
|
||||
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."
|
||||
msgstr "Recovery link cannot be emailed, user has no email address saved."
|
||||
|
||||
#: src/flows/stages/RedirectStage.ts
|
||||
#: src/pages/providers/saml/SAMLProviderForm.ts
|
||||
msgid "Redirect"
|
||||
msgstr "Redirect"
|
||||
|
@ -3748,6 +3775,10 @@ msgstr "Service Provider Binding"
|
|||
#~ msgid "Session"
|
||||
#~ msgstr "Session"
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Session ID"
|
||||
msgstr "Session ID"
|
||||
|
||||
#: src/pages/stages/user_login/UserLoginStageForm.ts
|
||||
msgid "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."
|
||||
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
|
||||
msgid "Show matched user"
|
||||
msgstr "Show matched user"
|
||||
|
||||
#: src/elements/Expand.ts
|
||||
msgid "Show more"
|
||||
msgstr "Show more"
|
||||
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
msgid "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)"
|
||||
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
|
||||
msgid "Stage type"
|
||||
msgstr "Stage type"
|
||||
|
@ -4505,6 +4556,10 @@ msgstr ""
|
|||
msgid "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
|
||||
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."
|
||||
|
@ -5276,6 +5331,10 @@ msgstr "Yes"
|
|||
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."
|
||||
|
||||
#: 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
|
||||
msgid "You're currently impersonating {0}. Click to stop."
|
||||
msgstr "You're currently impersonating {0}. Click to stop."
|
||||
|
|
|
@ -1145,6 +1145,10 @@ msgstr ""
|
|||
msgid "Creation Date"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Current plan cntext"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/applications/ApplicationForm.ts
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
msgid "Currently set to:"
|
||||
|
@ -1618,6 +1622,10 @@ msgstr ""
|
|||
msgid "Execute flow"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Execute with inspector"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/expression/ExpressionPolicyForm.ts
|
||||
msgid "Executes the python snippet to determine whether to allow or deny a request."
|
||||
msgstr ""
|
||||
|
@ -1785,6 +1793,11 @@ msgstr ""
|
|||
msgid "Flow Overview"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Flow inspector"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/plex/PlexSourceForm.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."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/RedirectStage.ts
|
||||
msgid "Follow redirect"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Force the user to configure an authenticator"
|
||||
msgstr ""
|
||||
|
@ -2342,6 +2359,7 @@ msgstr ""
|
|||
#: src/elements/table/Table.ts
|
||||
#: src/flows/FlowExecutor.ts
|
||||
#: src/flows/FlowExecutor.ts
|
||||
#: src/flows/FlowInspector.ts
|
||||
#: src/flows/access_denied/FlowAccessDenied.ts
|
||||
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.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."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Next stage"
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/oauth/UserRefreshList.ts
|
||||
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
|
@ -3071,6 +3093,10 @@ msgstr ""
|
|||
msgid "Placeholder"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Plan history"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
|
||||
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
|
||||
msgid "Please enter your TOTP Code"
|
||||
|
@ -3373,6 +3399,7 @@ msgstr ""
|
|||
msgid "Recovery link cannot be emailed, user has no email address saved."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/RedirectStage.ts
|
||||
#: src/pages/providers/saml/SAMLProviderForm.ts
|
||||
msgid "Redirect"
|
||||
msgstr ""
|
||||
|
@ -3740,6 +3767,10 @@ msgstr ""
|
|||
#~ msgid "Session"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "Session ID"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/user_login/UserLoginStageForm.ts
|
||||
msgid "Session duration"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/Expand.ts
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "Show matched user"
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/Expand.ts
|
||||
msgid "Show more"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
msgid "Shown as the Title in Flow pages."
|
||||
msgstr ""
|
||||
|
@ -3889,6 +3928,18 @@ msgstr ""
|
|||
msgid "Stage binding(s)"
|
||||
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
|
||||
msgid "Stage type"
|
||||
msgstr ""
|
||||
|
@ -4490,6 +4541,10 @@ msgstr ""
|
|||
msgid "These policies control which users can access this application."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowInspector.ts
|
||||
msgid "This flow is completed."
|
||||
msgstr ""
|
||||
|
||||
#: 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."
|
||||
msgstr ""
|
||||
|
@ -5259,6 +5314,10 @@ msgstr ""
|
|||
msgid "You can only select providers that match the type of the outpost."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/RedirectStage.ts
|
||||
msgid "You're about to be redirect to the following URL."
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
msgid "You're currently impersonating {0}. Click to stop."
|
||||
msgstr ""
|
||||
|
|
|
@ -104,7 +104,9 @@ export class FlowListPage extends TablePage<Flow> {
|
|||
slug: item.slug,
|
||||
})
|
||||
.then((link) => {
|
||||
window.open(`${link.link}?next=/%23${window.location.href}`);
|
||||
window.open(
|
||||
`${link.link}?inspector&next=/%23${window.location.href}`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -107,6 +107,21 @@ export class FlowViewPage extends LitElement {
|
|||
>
|
||||
${t`Execute`}
|
||||
</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>
|
||||
</dd>
|
||||
<dt class="pf-c-description-list__term">
|
||||
|
|
Reference in a new issue