flows: inspector (#1469)
* flows: add initial inspector Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: change naming a bit Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flow: add inspector frame Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: don't use shadydom when inspecting Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: add current stage to api Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/*: fix imports Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: deep-copy plan instead of just adding Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: ui Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: restrict inspector to admin Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add buttons to launch flow with inspector Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: don't automatically follow redirects when inspector is open Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: make current_plan optional, only require historry Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: handle error messages in inspector Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: improve UI when flow is done Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: add is_completed flag to inspector Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: fix monkeypatches for tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: add inspector tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ci: re-enable cache Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
ea4b920264
commit
f9ad102915
126
.github/workflows/ci-main.yml
vendored
126
.github/workflows/ci-main.yml
vendored
|
@ -25,14 +25,14 @@ jobs:
|
||||||
- uses: actions/setup-python@v2
|
- 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 a new issue