flows/inspector: add button to open flow inspector (#12656)

* flows: differentiate between flow inspector being available and open

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add overlay button to open inspector

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* fix perm check

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rewrite docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
Jens L.
2025-01-13 19:55:34 +01:00
committed by GitHub
parent 3098313981
commit 629d5df763
8 changed files with 62 additions and 24 deletions

View File

@ -159,9 +159,17 @@ class FlowPlan:
stage = final_stage(request=request, executor=temp_exec) stage = final_stage(request=request, executor=temp_exec)
return stage.dispatch(request) 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( return redirect_with_qs(
"authentik_core:if-flow", "authentik_core:if-flow",
request.GET, get_qs,
flow_slug=flow.slug, flow_slug=flow.slug,
) )

View File

@ -78,7 +78,9 @@ class FlowInspectorView(APIView):
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug) self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG: if settings.DEBUG:
return 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 return
raise Http404 raise Http404

View File

@ -49,7 +49,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": self.device_flow.slug, "flow_slug": self.device_flow.slug,
}, },
), )
+ "?"
+ urlencode({"inspector": "available"}),
) )
def test_device_init_post(self): def test_device_init_post(self):
@ -63,7 +65,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": self.device_flow.slug, "flow_slug": self.device_flow.slug,
}, },
), )
+ "?"
+ urlencode({"inspector": "available"}),
) )
res = self.api_client.get( res = self.api_client.get(
reverse( reverse(
@ -118,7 +122,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": provider.authorization_flow.slug, "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): def test_device_init_denied(self):

View File

@ -260,6 +260,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.wait_for_url( self.wait_for_url(
self.url( self.url(
"authentik_core:if-flow", "authentik_core:if-flow",
query={"inspector": "available"},
flow_slug=invalidation_flow.slug, flow_slug=invalidation_flow.slug,
) )
) )

View File

@ -10,6 +10,7 @@ from sys import stderr
from time import sleep from time import sleep
from typing import Any from typing import Any
from unittest.case import TestCase from unittest.case import TestCase
from urllib.parse import urlencode
from django.apps import apps from django.apps import apps
from django.contrib.staticfiles.testing import StaticLiveServerTestCase 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}", 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""" """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: def if_user_url(self, path: str | None = None) -> str:
"""same as self.url() but show URL in shell""" """same as self.url() but show URL in shell"""

View File

@ -191,7 +191,7 @@ export class FlowViewPage extends AKElement {
const finalURL = `${ const finalURL = `${
link.link link.link
}?${encodeURI( }?${encodeURI(
`inspector&next=/#${window.location.hash}`, `inspector=open&next=/#${window.location.hash}`,
)}`; )}`;
window.open(finalURL, "_blank"); window.open(finalURL, "_blank");
}) })

View File

@ -77,6 +77,9 @@ export class FlowExecutor extends Interface implements StageHost {
@state() @state()
inspectorOpen = false; inspectorOpen = false;
@state()
inspectorAvailable = false;
@state() @state()
flowInfo?: ContextualFlowInfo; flowInfo?: ContextualFlowInfo;
@ -160,14 +163,24 @@ export class FlowExecutor extends Interface implements StageHost {
padding: 0 2rem; padding: 0 2rem;
max-height: inherit; max-height: inherit;
} }
.inspector-toggle {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 100;
}
`); `);
} }
constructor() { constructor() {
super(); super();
this.ws = new WebsocketClient(); 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.inspectorOpen = true;
this.inspectorAvailable = true;
} else if (inspector === "available") {
this.inspectorAvailable = true;
} }
this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => { this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => {
this.inspectorOpen = !this.inspectorOpen; this.inspectorOpen = !this.inspectorOpen;
@ -231,12 +244,8 @@ export class FlowExecutor extends Interface implements StageHost {
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry(); configureSentry();
if ( if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) {
this.config?.capabilities.includes(CapabilitiesEnum.CanDebug) && this.inspectorAvailable = true;
// Only open inspector automatically in debug when we have enough space for it
window.innerWidth >= 768
) {
this.inspectorOpen = true;
} }
this.loading = true; this.loading = true;
try { try {
@ -545,6 +554,16 @@ export class FlowExecutor extends Interface implements StageHost {
</div> </div>
</div> </div>
</div> </div>
${(this.inspectorAvailable ?? !this.inspectorOpen)
? html`<button
class="inspector-toggle pf-c-button pf-m-primary"
@click=${() => {
this.inspectorOpen = true;
}}
>
<i class="fa fa-search-plus" aria-hidden="true"></i>
</button>`
: nothing}
${until(this.renderInspector())} ${until(this.renderInspector())}
</div> </div>
</div> </div>

View File

@ -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. 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 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.
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).
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. 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**. 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 :::info
Troubleshooting: 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 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.
- 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). :::
:::
### Flow Inspector Details ### Flow Inspector Details