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.events.models import Event, EventAction
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.types import PolicyRequest
@ -38,7 +39,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
error_string = exception_to_string(exc)
event = Event.new(
EventAction.PROPERTY_MAPPING_EXCEPTION,
expression=expression_source,

View File

@ -1,9 +1,9 @@
"""Challenge helpers"""
from dataclasses import asdict, is_dataclass
from enum import Enum
from traceback import format_tb
from typing import TYPE_CHECKING, Optional, TypedDict
from uuid import UUID
from rest_framework.request import Request
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
@ -11,6 +11,7 @@ from django.http import JsonResponse
from rest_framework.fields import CharField, ChoiceField, DictField
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.errors import exception_to_string
if TYPE_CHECKING:
from authentik.flows.stage import StageView
@ -90,32 +91,31 @@ class WithUserInfoChallenge(Challenge):
pending_user_avatar = CharField()
class FlowErrorChallenge(WithUserInfoChallenge):
class FlowErrorChallenge(Challenge):
"""Challenge class when an unhandled error occurs during a stage. Normal users
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()
error = CharField(required=False)
traceback = CharField(required=False)
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
error = kwargs.pop("error", None)
super().__init__(*args, **kwargs)
def __init__(self, request: Optional[Request] = None, error: Optional[Exception] = None):
super().__init__(data={})
if not request or not error:
return
self.request_id = request.request_id
self.initial_data["request_id"] = request.request_id
from authentik.core.models import USER_ATTRIBUTE_DEBUG
if request.user and request.user.is_authenticated:
if request.user.is_superuser or request.user.group_attributes(request).get(
USER_ATTRIBUTE_DEBUG, False
):
self.error = error
self.traceback = "".join(format_tb(self.error.__traceback__))
self.initial_data["error"] = str(error)
self.initial_data["traceback"] = exception_to_string(error)
class AccessDeniedChallenge(WithUserInfoChallenge):

View File

@ -255,7 +255,7 @@ class FlowExecutorView(APIView):
message=exception_to_string(exc),
).from_http(self.request)
challenge = FlowErrorChallenge(self.request, exc)
challenge.is_valid()
challenge.is_valid(raise_exception=True)
return to_stage_response(self.request, HttpChallengeResponse(challenge))
@extend_schema(

View File

@ -26611,7 +26611,7 @@ components:
ak-stage-consent: '#/components/schemas/ConsentChallenge'
ak-stage-dummy: '#/components/schemas/DummyChallenge'
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-provider-oauth2-device-code: '#/components/schemas/OAuthDeviceCodeChallenge'
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.
properties:
type:
$ref: '#/components/schemas/ChallengeChoices'
type: string
default: native
flow_info:
$ref: '#/components/schemas/ContextualFlowInfo'
component:
type: string
default: xak-flow-error
default: ak-stage-flow-error
response_errors:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
pending_user:
type: string
pending_user_avatar:
type: string
request_id:
type: string
error:
@ -27899,10 +27896,7 @@ components:
traceback:
type: string
required:
- pending_user
- pending_user_avatar
- request_id
- type
FlowImportResult:
type: object
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 { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/flow/stages/FlowErrorStage";
import "@goauthentik/flow/stages/RedirectStage";
import "@goauthentik/flow/stages/access_denied/AccessDeniedStage";
// Import webauthn-related stages to prevent issues on safari
@ -45,6 +46,7 @@ import {
ChallengeTypes,
CurrentTenant,
FlowChallengeResponseRequest,
FlowErrorChallenge,
FlowsApi,
LayoutEnum,
RedirectChallenge,
@ -107,10 +109,6 @@ export class FlowExecutor extends AKElement implements StageHost {
:host {
position: relative;
}
.ak-exception {
font-family: monospace;
overflow-x: scroll;
}
.pf-c-drawer__content {
background-color: transparent;
}
@ -254,27 +252,13 @@ export class FlowExecutor extends AKElement implements StageHost {
} else if (error instanceof Error) {
body = error.message;
}
this.challenge = {
type: ChallengeChoices.Shell,
body: `<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${t`Whoops!`}
</h1>
</header>
<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;
const challenge: FlowErrorChallenge = {
type: ChallengeChoices.Native,
component: "ak-stage-flow-error",
error: body,
requestId: "",
};
this.challenge = challenge as ChallengeTypes;
}
async renderChallengeNativeElement(): Promise<TemplateResult> {
@ -395,6 +379,12 @@ export class FlowExecutor extends AKElement implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></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:
break;
}

View File

@ -4,9 +4,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
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 { ifDefined } from "lit/directives/if-defined.js";
import AKGlobal from "@goauthentik/common/styles/authentik.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")
export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeResponseRequest> {
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 {
@ -32,29 +47,22 @@ export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeR
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<p>
${this.challenge?.error
? this.challenge.error
: t`Something went wrong! Please try again later.`}
</p>
${this.challenge?.traceback
? html`<pre class="ak-exception">
${this.challenge.error}${this.challenge.traceback}</pre
>`
: html``}
</div>
<h3 class="pf-c-title pf-m-3xl">
${this.challenge?.error
? this.challenge.error
: t`Something went wrong! Please try again later.`}
</h3>
${this.challenge?.traceback
? html`<div class="pf-c-form__group">
<pre class="ak-exception">${this.challenge.traceback}</pre>
</div>`
: html``}
${this.challenge?.requestId
? html`<div class="pf-c-form__group">
<p>${t`Request ID`}</p>
<code>${this.challenge.requestId}</code>
</div>`
: html``}
</form>
</div>
<footer class="pf-c-login__main-footer">

View File

@ -25,6 +25,7 @@ import {
ChallengeTypes,
CurrentTenant,
FlowChallengeResponseRequest,
FlowErrorChallenge,
FlowsApi,
RedirectChallenge,
ResponseError,
@ -123,27 +124,13 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
if (error instanceof Error) {
body = error.message;
}
this.challenge = {
type: ChallengeChoices.Shell,
body: `<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${t`Whoops!`}
</h1>
</header>
<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;
const challenge: FlowErrorChallenge = {
type: ChallengeChoices.Native,
component: "ak-stage-flow-error",
error: body,
requestId: "",
};
this.challenge = challenge as ChallengeTypes;
}
globalRefresh(): void {