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:
		
							
								
								
									
										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 New Issue
	
	Block a user
	 Jens L
					Jens L