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:
Jens Langhammer 2023-01-01 21:43:44 +01:00
parent d6d72489a7
commit 3980eea7c6
No known key found for this signature in database
7 changed files with 74 additions and 94 deletions

View File

@ -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,

View File

@ -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):

View File

@ -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(

View File

@ -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

View File

@ -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;
} }

View File

@ -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">

View File

@ -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 {