web/flows: rework error display, always use ak-stage-flow-error instead of shell
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
d6d72489a7
commit
3980eea7c6
|
@ -8,6 +8,7 @@ from django.http import HttpRequest
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
|
|
||||||
def handle_error(self, exc: Exception, expression_source: str):
|
def handle_error(self, exc: Exception, expression_source: str):
|
||||||
"""Exception Handler"""
|
"""Exception Handler"""
|
||||||
error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
|
error_string = exception_to_string(exc)
|
||||||
event = Event.new(
|
event = Event.new(
|
||||||
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
||||||
expression=expression_source,
|
expression=expression_source,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"""Challenge helpers"""
|
"""Challenge helpers"""
|
||||||
from dataclasses import asdict, is_dataclass
|
from dataclasses import asdict, is_dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from traceback import format_tb
|
|
||||||
from typing import TYPE_CHECKING, Optional, TypedDict
|
from typing import TYPE_CHECKING, Optional, TypedDict
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -11,6 +11,7 @@ from django.http import JsonResponse
|
||||||
from rest_framework.fields import CharField, ChoiceField, DictField
|
from rest_framework.fields import CharField, ChoiceField, DictField
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
|
@ -90,32 +91,31 @@ class WithUserInfoChallenge(Challenge):
|
||||||
pending_user_avatar = CharField()
|
pending_user_avatar = CharField()
|
||||||
|
|
||||||
|
|
||||||
class FlowErrorChallenge(WithUserInfoChallenge):
|
class FlowErrorChallenge(Challenge):
|
||||||
"""Challenge class when an unhandled error occurs during a stage. Normal users
|
"""Challenge class when an unhandled error occurs during a stage. Normal users
|
||||||
are shown an error message, superusers are shown a full stacktrace."""
|
are shown an error message, superusers are shown a full stacktrace."""
|
||||||
|
|
||||||
component = CharField(default="xak-flow-error")
|
type = CharField(default=ChallengeTypes.NATIVE.value)
|
||||||
|
component = CharField(default="ak-stage-flow-error")
|
||||||
|
|
||||||
request_id = CharField()
|
request_id = CharField()
|
||||||
|
|
||||||
error = CharField(required=False)
|
error = CharField(required=False)
|
||||||
traceback = CharField(required=False)
|
traceback = CharField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, request: Optional[Request] = None, error: Optional[Exception] = None):
|
||||||
request = kwargs.pop("request", None)
|
super().__init__(data={})
|
||||||
error = kwargs.pop("error", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if not request or not error:
|
if not request or not error:
|
||||||
return
|
return
|
||||||
self.request_id = request.request_id
|
self.initial_data["request_id"] = request.request_id
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
||||||
|
|
||||||
if request.user and request.user.is_authenticated:
|
if request.user and request.user.is_authenticated:
|
||||||
if request.user.is_superuser or request.user.group_attributes(request).get(
|
if request.user.is_superuser or request.user.group_attributes(request).get(
|
||||||
USER_ATTRIBUTE_DEBUG, False
|
USER_ATTRIBUTE_DEBUG, False
|
||||||
):
|
):
|
||||||
self.error = error
|
self.initial_data["error"] = str(error)
|
||||||
self.traceback = "".join(format_tb(self.error.__traceback__))
|
self.initial_data["traceback"] = exception_to_string(error)
|
||||||
|
|
||||||
|
|
||||||
class AccessDeniedChallenge(WithUserInfoChallenge):
|
class AccessDeniedChallenge(WithUserInfoChallenge):
|
||||||
|
|
|
@ -255,7 +255,7 @@ class FlowExecutorView(APIView):
|
||||||
message=exception_to_string(exc),
|
message=exception_to_string(exc),
|
||||||
).from_http(self.request)
|
).from_http(self.request)
|
||||||
challenge = FlowErrorChallenge(self.request, exc)
|
challenge = FlowErrorChallenge(self.request, exc)
|
||||||
challenge.is_valid()
|
challenge.is_valid(raise_exception=True)
|
||||||
return to_stage_response(self.request, HttpChallengeResponse(challenge))
|
return to_stage_response(self.request, HttpChallengeResponse(challenge))
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
|
14
schema.yml
14
schema.yml
|
@ -26611,7 +26611,7 @@ components:
|
||||||
ak-stage-consent: '#/components/schemas/ConsentChallenge'
|
ak-stage-consent: '#/components/schemas/ConsentChallenge'
|
||||||
ak-stage-dummy: '#/components/schemas/DummyChallenge'
|
ak-stage-dummy: '#/components/schemas/DummyChallenge'
|
||||||
ak-stage-email: '#/components/schemas/EmailChallenge'
|
ak-stage-email: '#/components/schemas/EmailChallenge'
|
||||||
xak-flow-error: '#/components/schemas/FlowErrorChallenge'
|
ak-stage-flow-error: '#/components/schemas/FlowErrorChallenge'
|
||||||
ak-stage-identification: '#/components/schemas/IdentificationChallenge'
|
ak-stage-identification: '#/components/schemas/IdentificationChallenge'
|
||||||
ak-provider-oauth2-device-code: '#/components/schemas/OAuthDeviceCodeChallenge'
|
ak-provider-oauth2-device-code: '#/components/schemas/OAuthDeviceCodeChallenge'
|
||||||
ak-provider-oauth2-device-code-finish: '#/components/schemas/OAuthDeviceCodeFinishChallenge'
|
ak-provider-oauth2-device-code-finish: '#/components/schemas/OAuthDeviceCodeFinishChallenge'
|
||||||
|
@ -27876,22 +27876,19 @@ components:
|
||||||
are shown an error message, superusers are shown a full stacktrace.
|
are shown an error message, superusers are shown a full stacktrace.
|
||||||
properties:
|
properties:
|
||||||
type:
|
type:
|
||||||
$ref: '#/components/schemas/ChallengeChoices'
|
type: string
|
||||||
|
default: native
|
||||||
flow_info:
|
flow_info:
|
||||||
$ref: '#/components/schemas/ContextualFlowInfo'
|
$ref: '#/components/schemas/ContextualFlowInfo'
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: xak-flow-error
|
default: ak-stage-flow-error
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ErrorDetail'
|
$ref: '#/components/schemas/ErrorDetail'
|
||||||
pending_user:
|
|
||||||
type: string
|
|
||||||
pending_user_avatar:
|
|
||||||
type: string
|
|
||||||
request_id:
|
request_id:
|
||||||
type: string
|
type: string
|
||||||
error:
|
error:
|
||||||
|
@ -27899,10 +27896,7 @@ components:
|
||||||
traceback:
|
traceback:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- pending_user
|
|
||||||
- pending_user_avatar
|
|
||||||
- request_id
|
- request_id
|
||||||
- type
|
|
||||||
FlowImportResult:
|
FlowImportResult:
|
||||||
type: object
|
type: object
|
||||||
description: Logs of an attempted flow import
|
description: Logs of an attempted flow import
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { first } from "@goauthentik/common/utils";
|
||||||
import { WebsocketClient } from "@goauthentik/common/ws";
|
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/LoadingOverlay";
|
import "@goauthentik/elements/LoadingOverlay";
|
||||||
|
import "@goauthentik/flow/stages/FlowErrorStage";
|
||||||
import "@goauthentik/flow/stages/RedirectStage";
|
import "@goauthentik/flow/stages/RedirectStage";
|
||||||
import "@goauthentik/flow/stages/access_denied/AccessDeniedStage";
|
import "@goauthentik/flow/stages/access_denied/AccessDeniedStage";
|
||||||
// Import webauthn-related stages to prevent issues on safari
|
// Import webauthn-related stages to prevent issues on safari
|
||||||
|
@ -45,6 +46,7 @@ import {
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
CurrentTenant,
|
CurrentTenant,
|
||||||
FlowChallengeResponseRequest,
|
FlowChallengeResponseRequest,
|
||||||
|
FlowErrorChallenge,
|
||||||
FlowsApi,
|
FlowsApi,
|
||||||
LayoutEnum,
|
LayoutEnum,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
|
@ -107,10 +109,6 @@ export class FlowExecutor extends AKElement implements StageHost {
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.ak-exception {
|
|
||||||
font-family: monospace;
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
.pf-c-drawer__content {
|
.pf-c-drawer__content {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -254,27 +252,13 @@ export class FlowExecutor extends AKElement implements StageHost {
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
body = error.message;
|
body = error.message;
|
||||||
}
|
}
|
||||||
this.challenge = {
|
const challenge: FlowErrorChallenge = {
|
||||||
type: ChallengeChoices.Shell,
|
type: ChallengeChoices.Native,
|
||||||
body: `<header class="pf-c-login__main-header">
|
component: "ak-stage-flow-error",
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
error: body,
|
||||||
${t`Whoops!`}
|
requestId: "",
|
||||||
</h1>
|
};
|
||||||
</header>
|
this.challenge = challenge as ChallengeTypes;
|
||||||
<div class="pf-c-login__main-body">
|
|
||||||
<h3>${t`Something went wrong! Please try again later.`}</h3>
|
|
||||||
<pre class="ak-exception">${body}</pre>
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__main-footer">
|
|
||||||
<ul class="pf-c-login__main-footer-links">
|
|
||||||
<li class="pf-c-login__main-footer-links-item">
|
|
||||||
<a class="pf-c-button pf-m-primary pf-m-block" href="/">
|
|
||||||
${t`Return`}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>`,
|
|
||||||
} as ChallengeTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderChallengeNativeElement(): Promise<TemplateResult> {
|
async renderChallengeNativeElement(): Promise<TemplateResult> {
|
||||||
|
@ -395,6 +379,12 @@ export class FlowExecutor extends AKElement implements StageHost {
|
||||||
.host=${this as StageHost}
|
.host=${this as StageHost}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
></ak-flow-provider-oauth2-code-finish>`;
|
></ak-flow-provider-oauth2-code-finish>`;
|
||||||
|
// Internal stages
|
||||||
|
case "ak-stage-flow-error":
|
||||||
|
return html`<ak-stage-flow-error
|
||||||
|
.host=${this as StageHost}
|
||||||
|
.challenge=${this.challenge}
|
||||||
|
></ak-stage-flow-error>`;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
|
@ -20,7 +19,23 @@ import { FlowChallengeResponseRequest, FlowErrorChallenge } from "@goauthentik/a
|
||||||
@customElement("ak-stage-flow-error")
|
@customElement("ak-stage-flow-error")
|
||||||
export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeResponseRequest> {
|
export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeResponseRequest> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, AKGlobal];
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFLogin,
|
||||||
|
PFForm,
|
||||||
|
PFFormControl,
|
||||||
|
PFTitle,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
|
pre {
|
||||||
|
overflow-x: scroll;
|
||||||
|
max-width: calc(
|
||||||
|
35rem - var(--pf-c-login__main-body--PaddingRight) -
|
||||||
|
var(--pf-c-login__main-body--PaddingRight)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -32,29 +47,22 @@ export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeR
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form">
|
<form class="pf-c-form">
|
||||||
<ak-form-static
|
<h3 class="pf-c-title pf-m-3xl">
|
||||||
class="pf-c-form__group"
|
${this.challenge?.error
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
? this.challenge.error
|
||||||
user=${this.challenge.pendingUser}
|
: t`Something went wrong! Please try again later.`}
|
||||||
>
|
</h3>
|
||||||
<div slot="link">
|
${this.challenge?.traceback
|
||||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
? html`<div class="pf-c-form__group">
|
||||||
>${t`Not you?`}</a
|
<pre class="ak-exception">${this.challenge.traceback}</pre>
|
||||||
>
|
</div>`
|
||||||
</div>
|
: html``}
|
||||||
</ak-form-static>
|
${this.challenge?.requestId
|
||||||
<div class="pf-c-form__group">
|
? html`<div class="pf-c-form__group">
|
||||||
<p>
|
<p>${t`Request ID`}</p>
|
||||||
${this.challenge?.error
|
<code>${this.challenge.requestId}</code>
|
||||||
? this.challenge.error
|
</div>`
|
||||||
: t`Something went wrong! Please try again later.`}
|
: html``}
|
||||||
</p>
|
|
||||||
${this.challenge?.traceback
|
|
||||||
? html`<pre class="ak-exception">
|
|
||||||
${this.challenge.error}${this.challenge.traceback}</pre
|
|
||||||
>`
|
|
||||||
: html``}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
CurrentTenant,
|
CurrentTenant,
|
||||||
FlowChallengeResponseRequest,
|
FlowChallengeResponseRequest,
|
||||||
|
FlowErrorChallenge,
|
||||||
FlowsApi,
|
FlowsApi,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
ResponseError,
|
ResponseError,
|
||||||
|
@ -123,27 +124,13 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
body = error.message;
|
body = error.message;
|
||||||
}
|
}
|
||||||
this.challenge = {
|
const challenge: FlowErrorChallenge = {
|
||||||
type: ChallengeChoices.Shell,
|
type: ChallengeChoices.Native,
|
||||||
body: `<header class="pf-c-login__main-header">
|
component: "ak-stage-flow-error",
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
error: body,
|
||||||
${t`Whoops!`}
|
requestId: "",
|
||||||
</h1>
|
};
|
||||||
</header>
|
this.challenge = challenge as ChallengeTypes;
|
||||||
<div class="pf-c-login__main-body">
|
|
||||||
<h3>${t`Something went wrong! Please try again later.`}</h3>
|
|
||||||
<pre class="ak-exception">${body}</pre>
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__main-footer">
|
|
||||||
<ul class="pf-c-login__main-footer-links">
|
|
||||||
<li class="pf-c-login__main-footer-links-item">
|
|
||||||
<a class="pf-c-button pf-m-primary pf-m-block" href="/">
|
|
||||||
${t`Return`}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>`,
|
|
||||||
} as ChallengeTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
globalRefresh(): void {
|
globalRefresh(): void {
|
||||||
|
|
Reference in New Issue