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 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run pylint |       - name: run pylint | ||||||
|         run: pipenv run pylint authentik tests lifecycle |         run: pipenv run pylint authentik tests lifecycle | ||||||
| @ -43,14 +43,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run black |       - name: run black | ||||||
|         run: pipenv run black --check authentik tests lifecycle |         run: pipenv run black --check authentik tests lifecycle | ||||||
| @ -61,14 +61,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run isort |       - name: run isort | ||||||
|         run: pipenv run isort --check authentik tests lifecycle |         run: pipenv run isort --check authentik tests lifecycle | ||||||
| @ -79,14 +79,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run bandit |       - name: run bandit | ||||||
|         run: pipenv run bandit -r authentik tests lifecycle |         run: pipenv run bandit -r authentik tests lifecycle | ||||||
| @ -113,14 +113,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run migrations |       - name: run migrations | ||||||
|         run: pipenv run python -m lifecycle.migrate |         run: pipenv run python -m lifecycle.migrate | ||||||
| @ -138,14 +138,14 @@ jobs: | |||||||
|           # Copy current, latest config to local |           # Copy current, latest config to local | ||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|           git checkout $(git describe --abbrev=0 --match 'version/*') |           git checkout $(git describe --abbrev=0 --match 'version/*') | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run migrations to stable |       - name: run migrations to stable | ||||||
|         run: pipenv run python -m lifecycle.migrate |         run: pipenv run python -m lifecycle.migrate | ||||||
| @ -168,14 +168,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
| @ -197,14 +197,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
| @ -236,14 +236,14 @@ jobs: | |||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
|           domain: ${{github.repository_owner}} |           domain: ${{github.repository_owner}} | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.6 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: | |         run: | | ||||||
|           scripts/ci_prepare.sh |           scripts/ci_prepare.sh | ||||||
|           docker-compose -f tests/e2e/ci.docker-compose.yml up -d |           docker-compose -f tests/e2e/ci.docker-compose.yml up -d | ||||||
|  | |||||||
| @ -30,7 +30,8 @@ from authentik.events.api.notification_transport import NotificationTransportVie | |||||||
| from authentik.flows.api.bindings import FlowStageBindingViewSet | from authentik.flows.api.bindings import FlowStageBindingViewSet | ||||||
| from authentik.flows.api.flows import FlowViewSet | from authentik.flows.api.flows import FlowViewSet | ||||||
| from authentik.flows.api.stages import StageViewSet | from authentik.flows.api.stages import StageViewSet | ||||||
| from authentik.flows.views import FlowExecutorView | from authentik.flows.views.executor import FlowExecutorView | ||||||
|  | from authentik.flows.views.inspector import FlowInspectorView | ||||||
| from authentik.outposts.api.outposts import OutpostViewSet | from authentik.outposts.api.outposts import OutpostViewSet | ||||||
| from authentik.outposts.api.service_connections import ( | from authentik.outposts.api.service_connections import ( | ||||||
|     DockerServiceConnectionViewSet, |     DockerServiceConnectionViewSet, | ||||||
| @ -228,6 +229,11 @@ urlpatterns = ( | |||||||
|             FlowExecutorView.as_view(), |             FlowExecutorView.as_view(), | ||||||
|             name="flow-executor", |             name="flow-executor", | ||||||
|         ), |         ), | ||||||
|  |         path( | ||||||
|  |             "flows/inspector/<slug:flow_slug>/", | ||||||
|  |             FlowInspectorView.as_view(), | ||||||
|  |             name="flow-inspector", | ||||||
|  |         ), | ||||||
|         path("sentry/", SentryTunnelView.as_view(), name="sentry"), |         path("sentry/", SentryTunnelView.as_view(), name="sentry"), | ||||||
|         path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), |         path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from django.http.request import HttpRequest | |||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
| from authentik.events.utils import cleanse_dict, sanitize_dict | from authentik.events.utils import cleanse_dict, sanitize_dict | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ from authentik.flows.planner import ( | |||||||
|     PLAN_CONTEXT_SSO, |     PLAN_CONTEXT_SSO, | ||||||
|     FlowPlanner, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.policies.utils import delete_none_keys | from authentik.policies.utils import delete_none_keys | ||||||
| from authentik.stages.password import BACKEND_INBUILT | from authentik.stages.password import BACKEND_INBUILT | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ | |||||||
|  |  | ||||||
| {% block head_before %} | {% block head_before %} | ||||||
| {{ block.super }} | {{ block.super }} | ||||||
| {% if flow.compatibility_mode %} | {% if flow.compatibility_mode and not inspector %} | ||||||
| <script>ShadyDOM = { force: !navigator.webdriver };</script> | <script>ShadyDOM = { force: !navigator.webdriver };</script> | ||||||
| {% endif %} | {% endif %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from django.test import TestCase | |||||||
| from authentik.core.auth import TokenBackend | from authentik.core.auth import TokenBackend | ||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.tests.utils import get_request | from authentik.lib.tests.utils import get_request | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -14,4 +14,5 @@ class FlowInterfaceView(TemplateView): | |||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: |     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | ||||||
|         kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) |         kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||||
|  |         kwargs["inspector"] = "inspector" in self.request.GET | ||||||
|         return super().get_context_data(**kwargs) |         return super().get_context_data(**kwargs) | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.core.signals import password_changed | |||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.events.tasks import event_notification_handler | from authentik.events.tasks import event_notification_handler | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.invitation.models import Invitation | from authentik.stages.invitation.models import Invitation | ||||||
| from authentik.stages.invitation.signals import invitation_used | from authentik.stages.invitation.signals import invitation_used | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cach | |||||||
| from authentik.flows.transfer.common import DataclassEncoder | from authentik.flows.transfer.common import DataclassEncoder | ||||||
| from authentik.flows.transfer.exporter import FlowExporter | from authentik.flows.transfer.exporter import FlowExporter | ||||||
| from authentik.flows.transfer.importer import FlowImporter | from authentik.flows.transfer.importer import FlowImporter | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ from authentik.flows.challenge import ( | |||||||
| ) | ) | ||||||
| from authentik.flows.models import InvalidResponseAction | from authentik.flows.models import InvalidResponseAction | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.views import FlowExecutorView | from authentik.flows.views.executor import FlowExecutorView | ||||||
|  |  | ||||||
| PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" | PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker | |||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction | ||||||
| from authentik.flows.planner import FlowPlan, FlowPlanner | from authentik.flows.planner import FlowPlan, FlowPlanner | ||||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | ||||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| @ -38,13 +38,13 @@ TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestFlowExecutor(APITestCase): | class TestFlowExecutor(APITestCase): | ||||||
|     """Test views logic""" |     """Test executor""" | ||||||
| 
 | 
 | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.request_factory = RequestFactory() |         self.request_factory = RequestFactory() | ||||||
| 
 | 
 | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_existing_plan_diff_flow(self): |     def test_existing_plan_diff_flow(self): | ||||||
| @ -62,7 +62,7 @@ class TestFlowExecutor(APITestCase): | |||||||
|         session.save() |         session.save() | ||||||
| 
 | 
 | ||||||
|         cancel_mock = MagicMock() |         cancel_mock = MagicMock() | ||||||
|         with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock): |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", cancel_mock): | ||||||
|             response = self.client.get( |             response = self.client.get( | ||||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), |                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|             ) |             ) | ||||||
| @ -70,7 +70,7 @@ class TestFlowExecutor(APITestCase): | |||||||
|             self.assertEqual(cancel_mock.call_count, 2) |             self.assertEqual(cancel_mock.call_count, 2) | ||||||
| 
 | 
 | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     @patch( |     @patch( | ||||||
| @ -105,7 +105,7 @@ class TestFlowExecutor(APITestCase): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_invalid_empty_flow(self): |     def test_invalid_empty_flow(self): | ||||||
| @ -124,7 +124,7 @@ class TestFlowExecutor(APITestCase): | |||||||
|         self.assertEqual(response.url, reverse("authentik_core:root-redirect")) |         self.assertEqual(response.url, reverse("authentik_core:root-redirect")) | ||||||
| 
 | 
 | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_invalid_flow_redirect(self): |     def test_invalid_flow_redirect(self): | ||||||
| @ -175,7 +175,7 @@ class TestFlowExecutor(APITestCase): | |||||||
|         self.assertEqual(len(plan.bindings), 1) |         self.assertEqual(len(plan.bindings), 1) | ||||||
| 
 | 
 | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_reevaluate_remove_last(self): |     def test_reevaluate_remove_last(self): | ||||||
							
								
								
									
										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 django.test import RequestFactory, TestCase | ||||||
|  |  | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views import FlowExecutorView | from authentik.flows.views.executor import FlowExecutorView | ||||||
| from authentik.lib.utils.reflection import all_subclasses | from authentik.lib.utils.reflection import all_subclasses | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from django.urls import reverse | |||||||
|  |  | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestHelperView(TestCase): | class TestHelperView(TestCase): | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from django.urls import path | from django.urls import path | ||||||
|  |  | ||||||
| from authentik.flows.models import FlowDesignation | from authentik.flows.models import FlowDesignation | ||||||
| from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow | from authentik.flows.views.executor import CancelView, ConfigureFlowInitView, ToDefaultFlow | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path( |     path( | ||||||
|  | |||||||
							
								
								
									
										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""" | """authentik multi-stage authentication engine""" | ||||||
|  | from copy import deepcopy | ||||||
| from traceback import format_tb | from traceback import format_tb | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
| 
 | 
 | ||||||
| @ -52,6 +53,7 @@ NEXT_ARG_NAME = "next" | |||||||
| SESSION_KEY_PLAN = "authentik_flows_plan" | SESSION_KEY_PLAN = "authentik_flows_plan" | ||||||
| SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" | SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" | ||||||
| SESSION_KEY_GET = "authentik_flows_get" | SESSION_KEY_GET = "authentik_flows_get" | ||||||
|  | SESSION_KEY_HISTORY = "authentik_flows_history" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def challenge_types(): | def challenge_types(): | ||||||
| @ -140,6 +142,7 @@ class FlowExecutorView(APIView): | |||||||
| 
 | 
 | ||||||
|         # Don't check session again as we've either already loaded the plan or we need to plan |         # Don't check session again as we've either already loaded the plan or we need to plan | ||||||
|         if not self.plan: |         if not self.plan: | ||||||
|  |             request.session[SESSION_KEY_HISTORY] = [] | ||||||
|             self._logger.debug("f(exec): No active Plan found, initiating planner") |             self._logger.debug("f(exec): No active Plan found, initiating planner") | ||||||
|             try: |             try: | ||||||
|                 self.plan = self._initiate_plan() |                 self.plan = self._initiate_plan() | ||||||
| @ -321,6 +324,7 @@ class FlowExecutorView(APIView): | |||||||
|             "f(exec): Stage ok", |             "f(exec): Stage ok", | ||||||
|             stage_class=class_to_path(self.current_stage_view.__class__), |             stage_class=class_to_path(self.current_stage_view.__class__), | ||||||
|         ) |         ) | ||||||
|  |         self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan)) | ||||||
|         self.plan.pop() |         self.plan.pop() | ||||||
|         self.request.session[SESSION_KEY_PLAN] = self.plan |         self.request.session[SESSION_KEY_PLAN] = self.plan | ||||||
|         if self.plan.bindings: |         if self.plan.bindings: | ||||||
| @ -368,6 +372,10 @@ class FlowExecutorView(APIView): | |||||||
|             SESSION_KEY_APPLICATION_PRE, |             SESSION_KEY_APPLICATION_PRE, | ||||||
|             SESSION_KEY_PLAN, |             SESSION_KEY_PLAN, | ||||||
|             SESSION_KEY_GET, |             SESSION_KEY_GET, | ||||||
|  |             # We don't delete the history on purpose, as a user might | ||||||
|  |             # still be inspecting it. | ||||||
|  |             # It's only deleted on a fresh executions | ||||||
|  |             # SESSION_KEY_HISTORY, | ||||||
|         ] |         ] | ||||||
|         for key in keys_to_delete: |         for key in keys_to_delete: | ||||||
|             if key in self.request.session: |             if key in self.request.session: | ||||||
							
								
								
									
										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 structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Application, Provider, User | from authentik.core.models import Application, Provider, User | ||||||
| from authentik.flows.views import SESSION_KEY_APPLICATION_PRE | from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.policies.denied import AccessDeniedResponse | from authentik.policies.denied import AccessDeniedResponse | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ from authentik.flows.planner import ( | |||||||
|     FlowPlanner, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ from authentik.core.models import Application | |||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.models import in_memory_stage | from authentik.flows.models import in_memory_stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.views import PolicyAccessView | from authentik.policies.views import PolicyAccessView | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ from authentik.core.api.sources import SourceSerializer | |||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.flows.challenge import RedirectChallenge | from authentik.flows.challenge import RedirectChallenge | ||||||
| from authentik.flows.views import to_stage_response | from authentik.flows.views.executor import to_stage_response | ||||||
| from authentik.sources.plex.models import PlexSource | from authentik.sources.plex.models import PlexSource | ||||||
| from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager | from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager | ||||||
|  |  | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ from authentik.flows.planner import ( | |||||||
|     PLAN_CONTEXT_SSO, |     PLAN_CONTEXT_SSO, | ||||||
|     FlowPlanner, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.policies.utils import delete_none_keys | from authentik.policies.utils import delete_none_keys | ||||||
| from authentik.sources.saml.exceptions import ( | from authentik.sources.saml.exceptions import ( | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ from authentik.flows.planner import ( | |||||||
|     FlowPlanner, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.providers.saml.utils.encoding import nice64 | from authentik.providers.saml.utils.encoding import nice64 | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.flows.challenge import ( | |||||||
| ) | ) | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views import InvalidStageError | from authentik.flows.views.executor import InvalidStageError | ||||||
| from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.captcha.models import CaptchaStage | from authentik.stages.captcha.models import CaptchaStage | ||||||
|  |  | ||||||
| # https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do | # https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.deny.models import DenyStage | from authentik.stages.deny.models import DenyStage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ from authentik.core.models import Token | |||||||
| from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes | from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views import SESSION_KEY_GET | from authentik.flows.views.executor import SESSION_KEY_GET | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.events.models import Event, EventAction | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.stage import QS_KEY_TOKEN | from authentik.stages.email.stage import QS_KEY_TOKEN | ||||||
|  |  | ||||||
| @ -90,7 +90,7 @@ class TestEmailStage(APITestCase): | |||||||
|         session.save() |         session.save() | ||||||
|         token: Token = Token.objects.get(user=self.user) |         token: Token = Token.objects.get(user=self.user) | ||||||
|  |  | ||||||
|         with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||||
|             # Call the executor shell to preseed the session |             # Call the executor shell to preseed the session | ||||||
|             url = reverse( |             url = reverse( | ||||||
|                 "authentik_api:flow-executor", |                 "authentik_api:flow-executor", | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ from authentik.core.models import Application, Source, User | |||||||
| from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes | from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | ||||||
| from authentik.flows.views import SESSION_KEY_APPLICATION_PRE, challenge_types | from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, challenge_types | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from authentik.stages.identification.signals import identification_failed | from authentik.stages.identification.signals import identification_failed | ||||||
| from authentik.stages.password.stage import authenticate | from authentik.stages.password.stage import authenticate | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from structlog.stdlib import get_logger | |||||||
|  |  | ||||||
| from authentik.flows.models import in_memory_stage | from authentik.flows.models import in_memory_stage | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views import SESSION_KEY_GET | from authentik.flows.views.executor import SESSION_KEY_GET | ||||||
| from authentik.stages.invitation.models import Invitation, InvitationStage | from authentik.stages.invitation.models import Invitation, InvitationStage | ||||||
| from authentik.stages.invitation.signals import invitation_used | from authentik.stages.invitation.signals import invitation_used | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
|  | |||||||
| @ -12,8 +12,8 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK | from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.invitation.models import Invitation, InvitationStage | from authentik.stages.invitation.models import Invitation, InvitationStage | ||||||
| from authentik.stages.invitation.stage import ( | from authentik.stages.invitation.stage import ( | ||||||
|     INVITATION_TOKEN_KEY, |     INVITATION_TOKEN_KEY, | ||||||
| @ -40,7 +40,7 @@ class TestUserLoginStage(APITestCase): | |||||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) |         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_without_invitation_fail(self): |     def test_without_invitation_fail(self): | ||||||
| @ -108,7 +108,7 @@ class TestUserLoginStage(APITestCase): | |||||||
|         data = {"foo": "bar"} |         data = {"foo": "bar"} | ||||||
|         invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) |         invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) | ||||||
|  |  | ||||||
|         with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||||
|             base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) |             base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|             args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex}) |             args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex}) | ||||||
|             response = self.client.get(base_url + f"?query={args}") |             response = self.client.get(base_url + f"?query={args}") | ||||||
| @ -140,7 +140,7 @@ class TestUserLoginStage(APITestCase): | |||||||
|         session[SESSION_KEY_PLAN] = plan |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |  | ||||||
|         with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||||
|             base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) |             base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|             response = self.client.get(base_url, follow=True) |             response = self.client.get(base_url, follow=True) | ||||||
|  |  | ||||||
|  | |||||||
| @ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK | from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.generators import generate_key | from authentik.lib.generators import generate_key | ||||||
| from authentik.stages.password import BACKEND_INBUILT | from authentik.stages.password import BACKEND_INBUILT | ||||||
| from authentik.stages.password.models import PasswordStage | from authentik.stages.password.models import PasswordStage | ||||||
| @ -39,7 +39,7 @@ class TestPasswordStage(APITestCase): | |||||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) |         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_without_user(self): |     def test_without_user(self): | ||||||
| @ -153,7 +153,7 @@ class TestPasswordStage(APITestCase): | |||||||
|         self.assertNotIn(SESSION_KEY_PLAN, self.client.session) |         self.assertNotIn(SESSION_KEY_PLAN, self.client.session) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     @patch( |     @patch( | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.policies.expression.models import ExpressionPolicy | from authentik.policies.expression.models import ExpressionPolicy | ||||||
| from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse | ||||||
| @ -161,7 +161,7 @@ class TestPromptStage(APITestCase): | |||||||
|  |  | ||||||
|         challenge_response = self.test_valid_challenge_with_policy() |         challenge_response = self.test_valid_challenge_with_policy() | ||||||
|  |  | ||||||
|         with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||||
|             response = self.client.post( |             response = self.client.post( | ||||||
|                 reverse( |                 reverse( | ||||||
|                     "authentik_api:flow-executor", |                     "authentik_api:flow-executor", | ||||||
|  | |||||||
| @ -10,8 +10,8 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK | from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.user_delete.models import UserDeleteStage | from authentik.stages.user_delete.models import UserDeleteStage | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -32,7 +32,7 @@ class TestUserDeleteStage(APITestCase): | |||||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) |         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_no_user(self): |     def test_no_user(self): | ||||||
|  | |||||||
| @ -11,8 +11,8 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK | from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.user_login.models import UserLoginStage | from authentik.stages.user_login.models import UserLoginStage | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -81,7 +81,7 @@ class TestUserLoginStage(APITestCase): | |||||||
|         self.assertEqual(list(self.client.session.keys()), []) |         self.assertEqual(list(self.client.session.keys()), []) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_without_user(self): |     def test_without_user(self): | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.password import BACKEND_INBUILT | from authentik.stages.password import BACKEND_INBUILT | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||||
| from authentik.stages.user_logout.models import UserLogoutStage | from authentik.stages.user_logout.models import UserLogoutStage | ||||||
|  | |||||||
| @ -13,8 +13,8 @@ from authentik.flows.challenge import ChallengeTypes | |||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK | from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK | ||||||
| from authentik.flows.views import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
| from authentik.stages.user_write.models import UserWriteStage | from authentik.stages.user_write.models import UserWriteStage | ||||||
|  |  | ||||||
| @ -112,7 +112,7 @@ class TestUserWriteStage(APITestCase): | |||||||
|         self.assertNotIn("some_ignored_attribute", user_qs.first().attributes) |         self.assertNotIn("some_ignored_attribute", user_qs.first().attributes) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_without_data(self): |     def test_without_data(self): | ||||||
| @ -142,7 +142,7 @@ class TestUserWriteStage(APITestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_blank_username(self): |     def test_blank_username(self): | ||||||
| @ -177,7 +177,7 @@ class TestUserWriteStage(APITestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.flows.views.to_stage_response", |         "authentik.flows.views.executor.to_stage_response", | ||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_duplicate_data(self): |     def test_duplicate_data(self): | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							| @ -4743,6 +4743,31 @@ paths: | |||||||
|           $ref: '#/components/schemas/ValidationError' |           $ref: '#/components/schemas/ValidationError' | ||||||
|         '403': |         '403': | ||||||
|           $ref: '#/components/schemas/GenericError' |           $ref: '#/components/schemas/GenericError' | ||||||
|  |   /flows/inspector/{flow_slug}/: | ||||||
|  |     get: | ||||||
|  |       operationId: flows_inspector_get | ||||||
|  |       description: Get current flow state and record it | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: flow_slug | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - flows | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/FlowInspection' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           description: No flow plan in session. | ||||||
|  |         '403': | ||||||
|  |           $ref: '#/components/schemas/GenericError' | ||||||
|   /flows/instances/: |   /flows/instances/: | ||||||
|     get: |     get: | ||||||
|       operationId: flows_instances_list |       operationId: flows_instances_list | ||||||
| @ -20273,6 +20298,45 @@ components: | |||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - diagram |       - diagram | ||||||
|  |     FlowInspection: | ||||||
|  |       type: object | ||||||
|  |       description: Serializer for inspect endpoint | ||||||
|  |       properties: | ||||||
|  |         plans: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             $ref: '#/components/schemas/FlowInspectorPlan' | ||||||
|  |         current_plan: | ||||||
|  |           $ref: '#/components/schemas/FlowInspectorPlan' | ||||||
|  |         is_completed: | ||||||
|  |           type: boolean | ||||||
|  |       required: | ||||||
|  |       - is_completed | ||||||
|  |       - plans | ||||||
|  |     FlowInspectorPlan: | ||||||
|  |       type: object | ||||||
|  |       description: Serializer for an active FlowPlan | ||||||
|  |       properties: | ||||||
|  |         current_stage: | ||||||
|  |           allOf: | ||||||
|  |           - $ref: '#/components/schemas/FlowStageBinding' | ||||||
|  |           readOnly: true | ||||||
|  |         next_planned_stage: | ||||||
|  |           allOf: | ||||||
|  |           - $ref: '#/components/schemas/FlowStageBinding' | ||||||
|  |           readOnly: true | ||||||
|  |         plan_context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|  |           readOnly: true | ||||||
|  |         session_id: | ||||||
|  |           type: string | ||||||
|  |           readOnly: true | ||||||
|  |       required: | ||||||
|  |       - current_stage | ||||||
|  |       - next_planned_stage | ||||||
|  |       - plan_context | ||||||
|  |       - session_id | ||||||
|     FlowRequest: |     FlowRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Flow Serializer |       description: Flow Serializer | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle"; | |||||||
| export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; | export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; | ||||||
| export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh"; | export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh"; | ||||||
| export const EVENT_WS_MESSAGE = "ak-ws-message"; | export const EVENT_WS_MESSAGE = "ak-ws-message"; | ||||||
|  | export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; | ||||||
|  |  | ||||||
| export const WS_MSG_TYPE_MESSAGE = "message"; | export const WS_MSG_TYPE_MESSAGE = "message"; | ||||||
| export const WS_MSG_TYPE_REFRESH = "refresh"; | export const WS_MSG_TYPE_REFRESH = "refresh"; | ||||||
|  | |||||||
| @ -11,10 +11,10 @@ export class Expand extends LitElement { | |||||||
|     expanded = false; |     expanded = false; | ||||||
|  |  | ||||||
|     @property() |     @property() | ||||||
|     textOpen = "Show less"; |     textOpen = t`Show less`; | ||||||
|  |  | ||||||
|     @property() |     @property() | ||||||
|     textClosed = "Show more"; |     textClosed = t`Show more`; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFExpandableSection]; |         return [PFExpandableSection]; | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import { until } from "lit/directives/until"; | |||||||
| import AKGlobal from "../authentik.css"; | import AKGlobal from "../authentik.css"; | ||||||
| import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css"; | import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | ||||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||||
| import PFLogin from "@patternfly/patternfly/components/Login/login.css"; | import PFLogin from "@patternfly/patternfly/components/Login/login.css"; | ||||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||||
| @ -26,12 +27,14 @@ import { | |||||||
| import { DEFAULT_CONFIG, tenant } from "../api/Config"; | import { DEFAULT_CONFIG, tenant } from "../api/Config"; | ||||||
| import { configureSentry } from "../api/Sentry"; | import { configureSentry } from "../api/Sentry"; | ||||||
| import { WebsocketClient } from "../common/ws"; | import { WebsocketClient } from "../common/ws"; | ||||||
| import { TITLE_DEFAULT } from "../constants"; | import { EVENT_FLOW_ADVANCE, TITLE_DEFAULT } from "../constants"; | ||||||
| import "../elements/LoadingOverlay"; | import "../elements/LoadingOverlay"; | ||||||
| import { DefaultTenant } from "../elements/sidebar/SidebarBrand"; | import { DefaultTenant } from "../elements/sidebar/SidebarBrand"; | ||||||
| import { first } from "../utils"; | import { first } from "../utils"; | ||||||
|  | import "./FlowInspector"; | ||||||
| import "./access_denied/FlowAccessDenied"; | import "./access_denied/FlowAccessDenied"; | ||||||
| import "./sources/plex/PlexLoginInit"; | import "./sources/plex/PlexLoginInit"; | ||||||
|  | import "./stages/RedirectStage"; | ||||||
| import "./stages/authenticator_duo/AuthenticatorDuoStage"; | import "./stages/authenticator_duo/AuthenticatorDuoStage"; | ||||||
| import "./stages/authenticator_static/AuthenticatorStaticStage"; | import "./stages/authenticator_static/AuthenticatorStaticStage"; | ||||||
| import "./stages/authenticator_totp/AuthenticatorTOTPStage"; | import "./stages/authenticator_totp/AuthenticatorTOTPStage"; | ||||||
| @ -59,7 +62,9 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|         // Assign the location as soon as we get the challenge and *not* in the render function |         // Assign the location as soon as we get the challenge and *not* in the render function | ||||||
|         // as the render function might be called multiple times, which will navigate multiple |         // as the render function might be called multiple times, which will navigate multiple | ||||||
|         // times and can invalidate oauth codes |         // times and can invalidate oauth codes | ||||||
|         if (value?.type === ChallengeChoices.Redirect) { |         // Also only auto-redirect when the inspector is open, so that a user can inspect the | ||||||
|  |         // redirect in the inspector | ||||||
|  |         if (value?.type === ChallengeChoices.Redirect && !this.inspectorOpen) { | ||||||
|             console.debug( |             console.debug( | ||||||
|                 "authentik/flows: redirecting to url from server", |                 "authentik/flows: redirecting to url from server", | ||||||
|                 (value as RedirectChallenge).to, |                 (value as RedirectChallenge).to, | ||||||
| @ -86,10 +91,14 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     tenant?: CurrentTenant; |     tenant?: CurrentTenant; | ||||||
|  |  | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     inspectorOpen: boolean; | ||||||
|  |  | ||||||
|     ws: WebsocketClient; |     ws: WebsocketClient; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css` |         return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal] | ||||||
|  |             .concat(css` | ||||||
|             .ak-hidden { |             .ak-hidden { | ||||||
|                 display: none; |                 display: none; | ||||||
|             } |             } | ||||||
| @ -100,6 +109,9 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                 font-family: monospace; |                 font-family: monospace; | ||||||
|                 overflow-x: scroll; |                 overflow-x: scroll; | ||||||
|             } |             } | ||||||
|  |             .pf-c-drawer__content { | ||||||
|  |                 background-color: transparent; | ||||||
|  |             } | ||||||
|         `); |         `); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -107,6 +119,7 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|         super(); |         super(); | ||||||
|         this.ws = new WebsocketClient(); |         this.ws = new WebsocketClient(); | ||||||
|         this.flowSlug = window.location.pathname.split("/")[3]; |         this.flowSlug = window.location.pathname.split("/")[3]; | ||||||
|  |         this.inspectorOpen = window.location.search.includes("inspector"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     setBackground(url: string): void { |     setBackground(url: string): void { | ||||||
| @ -130,6 +143,14 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                 flowChallengeResponseRequest: payload, |                 flowChallengeResponseRequest: payload, | ||||||
|             }) |             }) | ||||||
|             .then((data) => { |             .then((data) => { | ||||||
|  |                 if (this.inspectorOpen) { | ||||||
|  |                     window.dispatchEvent( | ||||||
|  |                         new CustomEvent(EVENT_FLOW_ADVANCE, { | ||||||
|  |                             bubbles: true, | ||||||
|  |                             composed: true, | ||||||
|  |                         }), | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|                 this.challenge = data; |                 this.challenge = data; | ||||||
|             }) |             }) | ||||||
|             .catch((e: Error | Response) => { |             .catch((e: Error | Response) => { | ||||||
| @ -150,6 +171,14 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                 query: window.location.search.substring(1), |                 query: window.location.search.substring(1), | ||||||
|             }) |             }) | ||||||
|             .then((challenge) => { |             .then((challenge) => { | ||||||
|  |                 if (this.inspectorOpen) { | ||||||
|  |                     window.dispatchEvent( | ||||||
|  |                         new CustomEvent(EVENT_FLOW_ADVANCE, { | ||||||
|  |                             bubbles: true, | ||||||
|  |                             composed: true, | ||||||
|  |                         }), | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|                 this.challenge = challenge; |                 this.challenge = challenge; | ||||||
|                 // Only set background on first update, flow won't change throughout execution |                 // Only set background on first update, flow won't change throughout execution | ||||||
|                 if (this.challenge?.flowInfo?.background) { |                 if (this.challenge?.flowInfo?.background) { | ||||||
| @ -199,6 +228,13 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|         } |         } | ||||||
|         switch (this.challenge.type) { |         switch (this.challenge.type) { | ||||||
|             case ChallengeChoices.Redirect: |             case ChallengeChoices.Redirect: | ||||||
|  |                 if (this.inspectorOpen) { | ||||||
|  |                     return html`<ak-stage-redirect | ||||||
|  |                         .host=${this as StageHost} | ||||||
|  |                         .challenge=${this.challenge} | ||||||
|  |                     > | ||||||
|  |                     </ak-stage-redirect>`; | ||||||
|  |                 } | ||||||
|                 return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> |                 return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> | ||||||
|                 </ak-empty-state>`; |                 </ak-empty-state>`; | ||||||
|             case ChallengeChoices.Shell: |             case ChallengeChoices.Shell: | ||||||
| @ -333,50 +369,74 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                     </filter> |                     </filter> | ||||||
|                 </svg> |                 </svg> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-login"> |             <div class="pf-c-page__drawer"> | ||||||
|                 <div class="ak-login-container"> |                 <div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}"> | ||||||
|                     <header class="pf-c-login__header"> |                     <div class="pf-c-drawer__main"> | ||||||
|                         <div class="pf-c-brand ak-brand"> |                         <div class="pf-c-drawer__content"> | ||||||
|                             <img |                             <div class="pf-c-drawer__body"> | ||||||
|                                 src="${first( |                                 <div class="pf-c-login"> | ||||||
|                                     this.tenant?.brandingLogo, |                                     <div class="ak-login-container"> | ||||||
|                                     DefaultTenant.brandingLogo, |                                         <header class="pf-c-login__header"> | ||||||
|                                 )}" |                                             <div class="pf-c-brand ak-brand"> | ||||||
|                                 alt="authentik icon" |                                                 <img | ||||||
|                             /> |                                                     src="${first( | ||||||
|  |                                                         this.tenant?.brandingLogo, | ||||||
|  |                                                         DefaultTenant.brandingLogo, | ||||||
|  |                                                     )}" | ||||||
|  |                                                     alt="authentik icon" | ||||||
|  |                                                 /> | ||||||
|  |                                             </div> | ||||||
|  |                                         </header> | ||||||
|  |                                         <div class="pf-c-login__main"> | ||||||
|  |                                             ${this.renderChallengeWrapper()} | ||||||
|  |                                         </div> | ||||||
|  |                                         <footer class="pf-c-login__footer"> | ||||||
|  |                                             <p></p> | ||||||
|  |                                             <ul class="pf-c-list pf-m-inline"> | ||||||
|  |                                                 ${until( | ||||||
|  |                                                     this.tenant?.uiFooterLinks?.map((link) => { | ||||||
|  |                                                         return html`<li> | ||||||
|  |                                                             <a href="${link.href || ""}" | ||||||
|  |                                                                 >${link.name}</a | ||||||
|  |                                                             > | ||||||
|  |                                                         </li>`; | ||||||
|  |                                                     }), | ||||||
|  |                                                 )} | ||||||
|  |                                                 ${this.tenant?.brandingTitle != "authentik" | ||||||
|  |                                                     ? html` | ||||||
|  |                                                           <li> | ||||||
|  |                                                               <a href="https://goauthentik.io" | ||||||
|  |                                                                   >${t`Powered by authentik`}</a | ||||||
|  |                                                               > | ||||||
|  |                                                           </li> | ||||||
|  |                                                       ` | ||||||
|  |                                                     : html``} | ||||||
|  |                                                 ${this.challenge?.flowInfo?.background?.startsWith( | ||||||
|  |                                                     "/static", | ||||||
|  |                                                 ) | ||||||
|  |                                                     ? html` | ||||||
|  |                                                           <li> | ||||||
|  |                                                               <a | ||||||
|  |                                                                   href="https://unsplash.com/@introspectivedsgn" | ||||||
|  |                                                                   >${t`Background image`}</a | ||||||
|  |                                                               > | ||||||
|  |                                                           </li> | ||||||
|  |                                                       ` | ||||||
|  |                                                     : html``} | ||||||
|  |                                             </ul> | ||||||
|  |                                         </footer> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </header> |  | ||||||
|                     <div class="pf-c-login__main">${this.renderChallengeWrapper()}</div> |                         <ak-flow-inspector | ||||||
|                     <footer class="pf-c-login__footer"> |                             class="pf-c-drawer__panel pf-m-width-33 ${this.inspectorOpen | ||||||
|                         <p></p> |                                 ? "" | ||||||
|                         <ul class="pf-c-list pf-m-inline"> |                                 : "display-none"}" | ||||||
|                             ${until( |                             ?hidden=${!this.inspectorOpen} | ||||||
|                                 this.tenant?.uiFooterLinks?.map((link) => { |                         ></ak-flow-inspector> | ||||||
|                                     return html`<li> |                     </div> | ||||||
|                                         <a href="${link.href || ""}">${link.name}</a> |  | ||||||
|                                     </li>`; |  | ||||||
|                                 }), |  | ||||||
|                             )} |  | ||||||
|                             ${this.tenant?.brandingTitle != "authentik" |  | ||||||
|                                 ? html` |  | ||||||
|                                       <li> |  | ||||||
|                                           <a href="https://goauthentik.io" |  | ||||||
|                                               >${t`Powered by authentik`}</a |  | ||||||
|                                           > |  | ||||||
|                                       </li> |  | ||||||
|                                   ` |  | ||||||
|                                 : html``} |  | ||||||
|                             ${this.challenge?.flowInfo?.background?.startsWith("/static") |  | ||||||
|                                 ? html` |  | ||||||
|                                       <li> |  | ||||||
|                                           <a href="https://unsplash.com/@introspectivedsgn" |  | ||||||
|                                               >${t`Background image`}</a |  | ||||||
|                                           > |  | ||||||
|                                       </li> |  | ||||||
|                                   ` |  | ||||||
|                                 : html``} |  | ||||||
|                         </ul> |  | ||||||
|                     </footer> |  | ||||||
|                 </div> |                 </div> | ||||||
|             </div>`; |             </div>`; | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										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" | msgid "Creation Date" | ||||||
| msgstr "Creation Date" | msgstr "Creation Date" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Current plan cntext" | ||||||
|  | msgstr "Current plan cntext" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationForm.ts | #: src/pages/applications/ApplicationForm.ts | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Currently set to:" | msgid "Currently set to:" | ||||||
| @ -1626,6 +1630,10 @@ msgstr "Execute" | |||||||
| msgid "Execute flow" | msgid "Execute flow" | ||||||
| msgstr "Execute flow" | msgstr "Execute flow" | ||||||
|  |  | ||||||
|  | #: src/pages/flows/FlowViewPage.ts | ||||||
|  | msgid "Execute with inspector" | ||||||
|  | msgstr "Execute with inspector" | ||||||
|  |  | ||||||
| #: src/pages/policies/expression/ExpressionPolicyForm.ts | #: src/pages/policies/expression/ExpressionPolicyForm.ts | ||||||
| msgid "Executes the python snippet to determine whether to allow or deny a request." | msgid "Executes the python snippet to determine whether to allow or deny a request." | ||||||
| msgstr "Executes the python snippet to determine whether to allow or deny a request." | msgstr "Executes the python snippet to determine whether to allow or deny a request." | ||||||
| @ -1793,6 +1801,11 @@ msgstr "Flow" | |||||||
| msgid "Flow Overview" | msgid "Flow Overview" | ||||||
| msgstr "Flow Overview" | msgstr "Flow Overview" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Flow inspector" | ||||||
|  | msgstr "Flow inspector" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts | #: src/pages/sources/oauth/OAuthSourceForm.ts | ||||||
| #: src/pages/sources/plex/PlexSourceForm.ts | #: src/pages/sources/plex/PlexSourceForm.ts | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts | #: src/pages/sources/saml/SAMLSourceForm.ts | ||||||
| @ -1860,6 +1873,10 @@ msgstr "Flows" | |||||||
| msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | ||||||
| msgstr "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | msgstr "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
|  | msgid "Follow redirect" | ||||||
|  | msgstr "Follow redirect" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Force the user to configure an authenticator" | msgid "Force the user to configure an authenticator" | ||||||
| msgstr "Force the user to configure an authenticator" | msgstr "Force the user to configure an authenticator" | ||||||
| @ -2350,6 +2367,7 @@ msgstr "Load servers" | |||||||
| #: src/elements/table/Table.ts | #: src/elements/table/Table.ts | ||||||
| #: src/flows/FlowExecutor.ts | #: src/flows/FlowExecutor.ts | ||||||
| #: src/flows/FlowExecutor.ts | #: src/flows/FlowExecutor.ts | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
| #: src/flows/access_denied/FlowAccessDenied.ts | #: src/flows/access_denied/FlowAccessDenied.ts | ||||||
| #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts | #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts | ||||||
| #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts | #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts | ||||||
| @ -2714,6 +2732,10 @@ msgstr "New version available!" | |||||||
| msgid "Newly created users are added to this group, if a group is selected." | msgid "Newly created users are added to this group, if a group is selected." | ||||||
| msgstr "Newly created users are added to this group, if a group is selected." | msgstr "Newly created users are added to this group, if a group is selected." | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Next stage" | ||||||
|  | msgstr "Next stage" | ||||||
|  |  | ||||||
| #: src/elements/oauth/UserRefreshList.ts | #: src/elements/oauth/UserRefreshList.ts | ||||||
| #: src/pages/applications/ApplicationCheckAccessForm.ts | #: src/pages/applications/ApplicationCheckAccessForm.ts | ||||||
| #: src/pages/crypto/CertificateKeyPairListPage.ts | #: src/pages/crypto/CertificateKeyPairListPage.ts | ||||||
| @ -3079,6 +3101,10 @@ msgstr "Persistent" | |||||||
| msgid "Placeholder" | msgid "Placeholder" | ||||||
| msgstr "Placeholder" | msgstr "Placeholder" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Plan history" | ||||||
|  | msgstr "Plan history" | ||||||
|  |  | ||||||
| #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts | #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts | ||||||
| #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts | #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts | ||||||
| msgid "Please enter your TOTP Code" | msgid "Please enter your TOTP Code" | ||||||
| @ -3381,6 +3407,7 @@ msgstr "Recovery keys" | |||||||
| msgid "Recovery link cannot be emailed, user has no email address saved." | msgid "Recovery link cannot be emailed, user has no email address saved." | ||||||
| msgstr "Recovery link cannot be emailed, user has no email address saved." | msgstr "Recovery link cannot be emailed, user has no email address saved." | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts | #: src/pages/providers/saml/SAMLProviderForm.ts | ||||||
| msgid "Redirect" | msgid "Redirect" | ||||||
| msgstr "Redirect" | msgstr "Redirect" | ||||||
| @ -3748,6 +3775,10 @@ msgstr "Service Provider Binding" | |||||||
| #~ msgid "Session" | #~ msgid "Session" | ||||||
| #~ msgstr "Session" | #~ msgstr "Session" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Session ID" | ||||||
|  | msgstr "Session ID" | ||||||
|  |  | ||||||
| #: src/pages/stages/user_login/UserLoginStageForm.ts | #: src/pages/stages/user_login/UserLoginStageForm.ts | ||||||
| msgid "Session duration" | msgid "Session duration" | ||||||
| msgstr "Session duration" | msgstr "Session duration" | ||||||
| @ -3794,10 +3825,18 @@ msgstr "Severity" | |||||||
| msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | ||||||
| msgstr "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | msgstr "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | ||||||
|  |  | ||||||
|  | #: src/elements/Expand.ts | ||||||
|  | msgid "Show less" | ||||||
|  | msgstr "Show less" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "Show matched user" | msgid "Show matched user" | ||||||
| msgstr "Show matched user" | msgstr "Show matched user" | ||||||
|  |  | ||||||
|  | #: src/elements/Expand.ts | ||||||
|  | msgid "Show more" | ||||||
|  | msgstr "Show more" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Shown as the Title in Flow pages." | msgid "Shown as the Title in Flow pages." | ||||||
| msgstr "Shown as the Title in Flow pages." | msgstr "Shown as the Title in Flow pages." | ||||||
| @ -3897,6 +3936,18 @@ msgstr "Stage Configuration" | |||||||
| msgid "Stage binding(s)" | msgid "Stage binding(s)" | ||||||
| msgstr "Stage binding(s)" | msgstr "Stage binding(s)" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage kind" | ||||||
|  | msgstr "Stage kind" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage name" | ||||||
|  | msgstr "Stage name" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage object" | ||||||
|  | msgstr "Stage object" | ||||||
|  |  | ||||||
| #: src/pages/flows/BoundStagesList.ts | #: src/pages/flows/BoundStagesList.ts | ||||||
| msgid "Stage type" | msgid "Stage type" | ||||||
| msgstr "Stage type" | msgstr "Stage type" | ||||||
| @ -4505,6 +4556,10 @@ msgstr "" | |||||||
| msgid "These policies control which users can access this application." | msgid "These policies control which users can access this application." | ||||||
| msgstr "These policies control which users can access this application." | msgstr "These policies control which users can access this application." | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "This flow is completed." | ||||||
|  | msgstr "This flow is completed." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | ||||||
| msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | ||||||
| @ -5276,6 +5331,10 @@ msgstr "Yes" | |||||||
| msgid "You can only select providers that match the type of the outpost." | msgid "You can only select providers that match the type of the outpost." | ||||||
| msgstr "You can only select providers that match the type of the outpost." | msgstr "You can only select providers that match the type of the outpost." | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
|  | msgid "You're about to be redirect to the following URL." | ||||||
|  | msgstr "You're about to be redirect to the following URL." | ||||||
|  |  | ||||||
| #: src/interfaces/AdminInterface.ts | #: src/interfaces/AdminInterface.ts | ||||||
| msgid "You're currently impersonating {0}. Click to stop." | msgid "You're currently impersonating {0}. Click to stop." | ||||||
| msgstr "You're currently impersonating {0}. Click to stop." | msgstr "You're currently impersonating {0}. Click to stop." | ||||||
|  | |||||||
| @ -1145,6 +1145,10 @@ msgstr "" | |||||||
| msgid "Creation Date" | msgid "Creation Date" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Current plan cntext" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationForm.ts | #: src/pages/applications/ApplicationForm.ts | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Currently set to:" | msgid "Currently set to:" | ||||||
| @ -1618,6 +1622,10 @@ msgstr "" | |||||||
| msgid "Execute flow" | msgid "Execute flow" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/flows/FlowViewPage.ts | ||||||
|  | msgid "Execute with inspector" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/policies/expression/ExpressionPolicyForm.ts | #: src/pages/policies/expression/ExpressionPolicyForm.ts | ||||||
| msgid "Executes the python snippet to determine whether to allow or deny a request." | msgid "Executes the python snippet to determine whether to allow or deny a request." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1785,6 +1793,11 @@ msgstr "" | |||||||
| msgid "Flow Overview" | msgid "Flow Overview" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Flow inspector" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts | #: src/pages/sources/oauth/OAuthSourceForm.ts | ||||||
| #: src/pages/sources/plex/PlexSourceForm.ts | #: src/pages/sources/plex/PlexSourceForm.ts | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts | #: src/pages/sources/saml/SAMLSourceForm.ts | ||||||
| @ -1852,6 +1865,10 @@ msgstr "" | |||||||
| msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | msgid "Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
|  | msgid "Follow redirect" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Force the user to configure an authenticator" | msgid "Force the user to configure an authenticator" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2342,6 +2359,7 @@ msgstr "" | |||||||
| #: src/elements/table/Table.ts | #: src/elements/table/Table.ts | ||||||
| #: src/flows/FlowExecutor.ts | #: src/flows/FlowExecutor.ts | ||||||
| #: src/flows/FlowExecutor.ts | #: src/flows/FlowExecutor.ts | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
| #: src/flows/access_denied/FlowAccessDenied.ts | #: src/flows/access_denied/FlowAccessDenied.ts | ||||||
| #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts | #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts | ||||||
| #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts | #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts | ||||||
| @ -2706,6 +2724,10 @@ msgstr "" | |||||||
| msgid "Newly created users are added to this group, if a group is selected." | msgid "Newly created users are added to this group, if a group is selected." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Next stage" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/elements/oauth/UserRefreshList.ts | #: src/elements/oauth/UserRefreshList.ts | ||||||
| #: src/pages/applications/ApplicationCheckAccessForm.ts | #: src/pages/applications/ApplicationCheckAccessForm.ts | ||||||
| #: src/pages/crypto/CertificateKeyPairListPage.ts | #: src/pages/crypto/CertificateKeyPairListPage.ts | ||||||
| @ -3071,6 +3093,10 @@ msgstr "" | |||||||
| msgid "Placeholder" | msgid "Placeholder" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Plan history" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts | #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts | ||||||
| #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts | #: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts | ||||||
| msgid "Please enter your TOTP Code" | msgid "Please enter your TOTP Code" | ||||||
| @ -3373,6 +3399,7 @@ msgstr "" | |||||||
| msgid "Recovery link cannot be emailed, user has no email address saved." | msgid "Recovery link cannot be emailed, user has no email address saved." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts | #: src/pages/providers/saml/SAMLProviderForm.ts | ||||||
| msgid "Redirect" | msgid "Redirect" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3740,6 +3767,10 @@ msgstr "" | |||||||
| #~ msgid "Session" | #~ msgid "Session" | ||||||
| #~ msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Session ID" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/user_login/UserLoginStageForm.ts | #: src/pages/stages/user_login/UserLoginStageForm.ts | ||||||
| msgid "Session duration" | msgid "Session duration" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3786,10 +3817,18 @@ msgstr "" | |||||||
| msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | msgid "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/elements/Expand.ts | ||||||
|  | msgid "Show less" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "Show matched user" | msgid "Show matched user" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/elements/Expand.ts | ||||||
|  | msgid "Show more" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Shown as the Title in Flow pages." | msgid "Shown as the Title in Flow pages." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3889,6 +3928,18 @@ msgstr "" | |||||||
| msgid "Stage binding(s)" | msgid "Stage binding(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage kind" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage name" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "Stage object" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/BoundStagesList.ts | #: src/pages/flows/BoundStagesList.ts | ||||||
| msgid "Stage type" | msgid "Stage type" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -4490,6 +4541,10 @@ msgstr "" | |||||||
| msgid "These policies control which users can access this application." | msgid "These policies control which users can access this application." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/FlowInspector.ts | ||||||
|  | msgid "This flow is completed." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -5259,6 +5314,10 @@ msgstr "" | |||||||
| msgid "You can only select providers that match the type of the outpost." | msgid "You can only select providers that match the type of the outpost." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/stages/RedirectStage.ts | ||||||
|  | msgid "You're about to be redirect to the following URL." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/interfaces/AdminInterface.ts | #: src/interfaces/AdminInterface.ts | ||||||
| msgid "You're currently impersonating {0}. Click to stop." | msgid "You're currently impersonating {0}. Click to stop." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
| @ -104,7 +104,9 @@ export class FlowListPage extends TablePage<Flow> { | |||||||
|                                 slug: item.slug, |                                 slug: item.slug, | ||||||
|                             }) |                             }) | ||||||
|                             .then((link) => { |                             .then((link) => { | ||||||
|                                 window.open(`${link.link}?next=/%23${window.location.href}`); |                                 window.open( | ||||||
|  |                                     `${link.link}?inspector&next=/%23${window.location.href}`, | ||||||
|  |                                 ); | ||||||
|                             }); |                             }); | ||||||
|                     }} |                     }} | ||||||
|                 > |                 > | ||||||
|  | |||||||
| @ -107,6 +107,21 @@ export class FlowViewPage extends LitElement { | |||||||
|                                                 > |                                                 > | ||||||
|                                                     ${t`Execute`} |                                                     ${t`Execute`} | ||||||
|                                                 </button> |                                                 </button> | ||||||
|  |                                                 <button | ||||||
|  |                                                     class="pf-c-button pf-m-secondary" | ||||||
|  |                                                     @click=${() => { | ||||||
|  |                                                         new FlowsApi(DEFAULT_CONFIG) | ||||||
|  |                                                             .flowsInstancesExecuteRetrieve({ | ||||||
|  |                                                                 slug: this.flow.slug, | ||||||
|  |                                                             }) | ||||||
|  |                                                             .then((link) => { | ||||||
|  |                                                                 const finalURL = `${link.link}?inspector&next=/%23${window.location.hash}`; | ||||||
|  |                                                                 window.open(finalURL, "_blank"); | ||||||
|  |                                                             }); | ||||||
|  |                                                     }} | ||||||
|  |                                                 > | ||||||
|  |                                                     ${t`Execute with inspector`} | ||||||
|  |                                                 </button> | ||||||
|                                             </div> |                                             </div> | ||||||
|                                         </dd> |                                         </dd> | ||||||
|                                         <dt class="pf-c-description-list__term"> |                                         <dt class="pf-c-description-list__term"> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L