diff --git a/authentik/flows/planner.py b/authentik/flows/planner.py index 1ede566e78..e2a56062cc 100644 --- a/authentik/flows/planner.py +++ b/authentik/flows/planner.py @@ -159,9 +159,17 @@ class FlowPlan: stage = final_stage(request=request, executor=temp_exec) return stage.dispatch(request) + get_qs = request.GET.copy() + if request.user.is_authenticated and ( + # Object-scoped permission or global permission + request.user.has_perm("authentik_flows.inspect_flow", flow) + or request.user.has_perm("authentik_flows.inspect_flow") + ): + get_qs["inspector"] = "available" + return redirect_with_qs( "authentik_core:if-flow", - request.GET, + get_qs, flow_slug=flow.slug, ) diff --git a/authentik/flows/views/inspector.py b/authentik/flows/views/inspector.py index 3c08a81621..2185a6191d 100644 --- a/authentik/flows/views/inspector.py +++ b/authentik/flows/views/inspector.py @@ -78,7 +78,9 @@ class FlowInspectorView(APIView): self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug) if settings.DEBUG: return - if request.user.has_perm("authentik_flow.inspect_flow", self.flow): + if request.user.has_perm( + "authentik_flows.inspect_flow", self.flow + ) or request.user.has_perm("authentik_flows.inspect_flow"): return raise Http404 diff --git a/authentik/providers/oauth2/tests/test_device_init.py b/authentik/providers/oauth2/tests/test_device_init.py index e503bc0152..5fdc8bb924 100644 --- a/authentik/providers/oauth2/tests/test_device_init.py +++ b/authentik/providers/oauth2/tests/test_device_init.py @@ -49,7 +49,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): kwargs={ "flow_slug": self.device_flow.slug, }, - ), + ) + + "?" + + urlencode({"inspector": "available"}), ) def test_device_init_post(self): @@ -63,7 +65,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): kwargs={ "flow_slug": self.device_flow.slug, }, - ), + ) + + "?" + + urlencode({"inspector": "available"}), ) res = self.api_client.get( reverse( @@ -118,7 +122,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): kwargs={ "flow_slug": provider.authorization_flow.slug, }, - ), + ) + + "?" + + urlencode({"inspector": "available"}), }, ) @@ -150,7 +156,7 @@ class TesOAuth2DeviceInit(OAuthTestCase): }, ) + "?" - + urlencode({QS_KEY_CODE: token.user_code}), + + urlencode({QS_KEY_CODE: token.user_code, "inspector": "available"}), ) def test_device_init_denied(self): diff --git a/tests/e2e/test_provider_oauth2_grafana.py b/tests/e2e/test_provider_oauth2_grafana.py index 14816abf56..101c42d48b 100644 --- a/tests/e2e/test_provider_oauth2_grafana.py +++ b/tests/e2e/test_provider_oauth2_grafana.py @@ -260,6 +260,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): self.wait_for_url( self.url( "authentik_core:if-flow", + query={"inspector": "available"}, flow_slug=invalidation_flow.slug, ) ) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index d4a27334e9..dda9c374a5 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -10,6 +10,7 @@ from sys import stderr from time import sleep from typing import Any from unittest.case import TestCase +from urllib.parse import urlencode from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase @@ -209,9 +210,12 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): f"URL {self.driver.current_url} doesn't match expected URL {desired_url}", ) - def url(self, view, **kwargs) -> str: + def url(self, view, query: dict | None = None, **kwargs) -> str: """reverse `view` with `**kwargs` into full URL using live_server_url""" - return self.live_server_url + reverse(view, kwargs=kwargs) + url = self.live_server_url + reverse(view, kwargs=kwargs) + if query: + return url + "?" + urlencode(query) + return url def if_user_url(self, path: str | None = None) -> str: """same as self.url() but show URL in shell""" diff --git a/web/src/admin/flows/FlowViewPage.ts b/web/src/admin/flows/FlowViewPage.ts index 37a05af838..67ed848d7a 100644 --- a/web/src/admin/flows/FlowViewPage.ts +++ b/web/src/admin/flows/FlowViewPage.ts @@ -191,7 +191,7 @@ export class FlowViewPage extends AKElement { const finalURL = `${ link.link }?${encodeURI( - `inspector&next=/#${window.location.hash}`, + `inspector=open&next=/#${window.location.hash}`, )}`; window.open(finalURL, "_blank"); }) diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index e857cbc8ba..63efdeff18 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -77,6 +77,9 @@ export class FlowExecutor extends Interface implements StageHost { @state() inspectorOpen = false; + @state() + inspectorAvailable = false; + @state() flowInfo?: ContextualFlowInfo; @@ -160,14 +163,24 @@ export class FlowExecutor extends Interface implements StageHost { padding: 0 2rem; max-height: inherit; } + .inspector-toggle { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 100; + } `); } constructor() { super(); this.ws = new WebsocketClient(); - if (window.location.search.includes("inspector")) { + const inspector = new URL(window.location.toString()).searchParams.get("inspector"); + if (inspector === "" || inspector === "open") { this.inspectorOpen = true; + this.inspectorAvailable = true; + } else if (inspector === "available") { + this.inspectorAvailable = true; } this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => { this.inspectorOpen = !this.inspectorOpen; @@ -231,12 +244,8 @@ export class FlowExecutor extends Interface implements StageHost { async firstUpdated(): Promise { configureSentry(); - if ( - this.config?.capabilities.includes(CapabilitiesEnum.CanDebug) && - // Only open inspector automatically in debug when we have enough space for it - window.innerWidth >= 768 - ) { - this.inspectorOpen = true; + if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { + this.inspectorAvailable = true; } this.loading = true; try { @@ -545,6 +554,16 @@ export class FlowExecutor extends Interface implements StageHost { + ${(this.inspectorAvailable ?? !this.inspectorOpen) + ? html`` + : nothing} ${until(this.renderInspector())} diff --git a/website/docs/add-secure-apps/flows-stages/flow/inspector.md b/website/docs/add-secure-apps/flows-stages/flow/inspector.md index d43d7601ae..902d973eb2 100644 --- a/website/docs/add-secure-apps/flows-stages/flow/inspector.md +++ b/website/docs/add-secure-apps/flows-stages/flow/inspector.md @@ -14,13 +14,13 @@ As shown in the screenshot below, the flow inspector displays next to the select Be aware that when running a flow with the inspector enabled, the flow is still executed normally. This means that for example, a [User write](../stages/user_write.md) stage _will_ write user data. ::: -### Permissions and debug mode - -By default, the inspector is only enabled when the currently authenticated user is a superuser, OR if a user has been granted the [permission](../../../users-sources/access-control/permissions.md) **Can inspect a Flow's execution** (or is a user assigned to role with the permission). +The inspector is accessible to users that have been granted the [permission](../../../users-sources/access-control/permissions.md) **Can inspect a Flow's execution**, either directly or through a role. Superusers can always inspect flow executions. When developing authentik with the debug mode enabled, the inspector is enabled by default and can be accessed by both unauthenticated users and standard users. However the debug mode should only be used for the development of authentik. So unless you are a developer and need the more verbose error information, the best practice for using the flow inspector is to assign the permission, not use debug mode. -### Open the Flow Inspector +Starting with authentik 2025.2, for users with appropriate permissions to access the inspector a button is shown in the top right of the [default flow executor](./executors/if-flow.md) which opens the flow inspector. + +### Manually running a flow with the inspector 1. To access the inspector, open the Admin interface and navigate to **Flows and Stages -> Flows**. @@ -35,10 +35,8 @@ Alternatively, a user with the correct permission can launch the inspector by ad :::info Troubleshooting: -- If the flow inspector does not launch and a "Bad request" error displays, this is likely either because you selected a flow that is not defined in your instance or the flow has a policy bound directly to it that prevents access, so the inspector won't open because the flow can't run results. -- If the flow inspector launches but is empty, you can refresh the browser or advance the flow to load the inspector. This can occur when a race condition happens (the inspector tries to fetch the data before the flow plan is fully planned and as such the panel just shows blank). - -::: +- If the flow inspector does not launch and a "Bad request" error displays, this is likely either because you selected a flow that has a policy bound directly to it that prevents access (so the inspector won't open because the flow can't be executed) or because you do not have view permission on that specific flow. + ::: ### Flow Inspector Details