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:
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
@ -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"""
|
||||
|
@ -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");
|
||||
})
|
||||
|
@ -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<void> {
|
||||
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 {
|
||||
</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())}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user