import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { EventContext, EventModel, EventWithContext } from "@goauthentik/common/events";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand";
import "@goauthentik/elements/Spinner";
import { PFSize } from "@goauthentik/elements/Spinner";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { until } from "lit/directives/until.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventActions, FlowsApi } from "@goauthentik/api";
type Pair = [string, string | number | EventContext | EventModel | string[] | TemplateResult];
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
// This is the template message body with our stacktrace passed to github via a querystring. It is
// 702 bytes long in UTF-8. [As of July
// 2023](https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/),
// the longest URL (not query string, **URL**) passable via this method is 2048 bytes. This is a bit
// of a hack, but it will get the top of the context across even if it exceeds the limit of the more
// restrictive browsers.
const githubIssueMessageBody = (context: EventContext) => `
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Stacktrace from authentik
\`\`\`
${context.message as string}
\`\`\`
**Version and Deployment (please complete the following information):**
- authentik version: ${VERSION}
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.
`;
@customElement("ak-event-info")
export class EventInfo extends AKElement {
@property({ attribute: false })
event!: EventWithContext;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFFlex,
PFCard,
PFList,
PFDescriptionList,
css`
code {
display: block;
white-space: pre-wrap;
word-break: break-all;
}
.pf-l-flex {
justify-content: space-between;
}
.pf-l-flex__item {
min-width: 25%;
}
iframe {
width: 100%;
height: 50rem;
}
`,
];
}
renderDescriptionGroup([term, description]: Pair) {
return html`
`;
}
getModelInfo(context: EventModel): TemplateResult {
if (context === null) {
return html`-`;
}
const modelFields: Pair[] = [
[msg("UID"), context.pk],
[msg("Name"), context.name],
[msg("App"), context.app],
[msg("Model Name"), context.model_name],
];
return html`
${map(modelFields, this.renderDescriptionGroup)}
`;
}
getEmailInfo(context: EventContext): TemplateResult {
if (context === null) {
return html`-`;
}
// prettier-ignore
const emailFields: Pair[] = [
[msg("Message"), context.message],
[msg("Subject"), context.subject],
[msg("From"), context.from_email],
[msg("To"), html`${(context.to_email as string[]).map((to) => {
return html`${to}`;
})}`],
];
return html`
${map(emailFields, this.renderDescriptionGroup)}
`;
}
renderDefaultResponse(): TemplateResult {
return html`
${msg("Context")}
${JSON.stringify(this.event?.context, null, 4)}
${msg("User")}
${JSON.stringify(this.event?.user, null, 4)}
`;
}
buildGitHubIssueUrl(context: EventContext): string {
const httpRequest = this.event.context.http_request as EventContext;
const title = httpRequest ? `${httpRequest?.method} ${httpRequest?.path}` : "";
return [
"https://github.com/goauthentik/authentik/issues/new",
"?labels=bug,from_authentik",
`&title=${encodeURIComponent(title)}`,
`&body=${encodeURIComponent(githubIssueMessageBody(context))}`,
]
.join("")
.trim();
}
// It's commonplace not to put the return type on most functions in Typescript. In this case,
// however, putting this return type creates a virtuous check of *all* the subrenderers to
// ensure that all of them return what we're expecting.
render(): TemplateResult {
if (!this.event) {
return html``;
}
switch (this.event?.action) {
case EventActions.ModelCreated:
case EventActions.ModelUpdated:
case EventActions.ModelDeleted:
return this.renderModelChanged();
case EventActions.AuthorizeApplication:
return this.renderAuthorizeApplication();
case EventActions.EmailSent:
return this.renderEmailSent();
case EventActions.SecretView:
return this.renderSecretView();
case EventActions.SystemException:
return this.renderSystemException();
case EventActions.PropertyMappingException:
return this.renderPropertyMappingException();
case EventActions.PolicyException:
return this.renderPolicyException();
case EventActions.PolicyExecution:
return this.renderPolicyExecution();
case EventActions.ConfigurationError:
return this.renderConfigurationError();
case EventActions.UpdateAvailable:
return this.renderUpdateAvailable();
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
case EventActions.Login:
return this.renderLogin();
case EventActions.LoginFailed:
return this.renderLoginFailed();
case EventActions.Logout:
return this.renderLogout();
case EventActions.SystemTaskException:
return this.renderSystemTaskException();
default:
return this.renderDefaultResponse();
}
}
renderModelChanged() {
return html`
${msg("Affected model:")}
${this.getModelInfo(this.event.context?.model as EventModel)}
`;
}
renderAuthorizeApplication() {
return html`
${msg("Authorized application:")}
${this.getModelInfo(
this.event.context.authorized_application as EventModel,
)}
${msg("Using flow")}
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
flowUuid: this.event.context.flow as string,
})
.then((resp) => {
return html`${resp.results[0].name}`;
}),
html``,
)}
${this.renderDefaultResponse()}`;
}
renderEmailSent() {
return html`${msg("Email info:")}
${this.getEmailInfo(this.event.context)}
`;
}
renderSecretView() {
return html` ${msg("Secret:")}
${this.getModelInfo(this.event.context.secret as EventModel)}`;
}
renderSystemException() {
return html`
${msg("Exception")}
${this.event.context.message}
${this.renderDefaultResponse()}`;
}
renderPropertyMappingException() {
return html`
${msg("Exception")}
${this.event.context.message || this.event.context.error}
${msg("Expression")}
${this.event.context.expression}
${this.renderDefaultResponse()}`;
}
renderPolicyException() {
return html`
${msg("Binding")}
${this.getModelInfo(this.event.context.binding as EventModel)}
${msg("Request")}
-
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext).obj as EventModel,
)}
-
${msg("Context")}:
${JSON.stringify(
(this.event.context.request as EventContext).context,
null,
4,
)}
${msg("Exception")}
${this.event.context.message || this.event.context.error}
${this.renderDefaultResponse()}`;
}
renderPolicyExecution() {
return html`
${msg("Binding")}
${this.getModelInfo(this.event.context.binding as EventModel)}
${msg("Request")}
-
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext).obj as EventModel,
)}
-
${msg("Context")}:
${JSON.stringify(
(this.event.context.request as EventContext).context,
null,
4,
)}
${msg("Result")}
-
${msg("Passing")}:
${(this.event.context.result as EventContext).passing}
-
${msg("Messages")}:
${(
(this.event.context.result as EventContext)
.messages as string[]
).map((msg) => {
return html`- ${msg}
`;
})}
${this.renderDefaultResponse()}`;
}
renderConfigurationError() {
return html`${this.event.context.message}
${this.renderDefaultResponse()}`;
}
renderUpdateAvailable() {
return html`${msg("New version available")}
${this.event.context.new_version}
`;
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
}
renderLogin() {
if ("using_source" in this.event.context) {
return html`
${msg("Using source")}
${this.getModelInfo(this.event.context.using_source as EventModel)}
`;
}
return this.renderDefaultResponse();
}
renderLoginFailed() {
return html`
${msg(str`Attempted to log in as ${this.event.context.username}`)}
${this.renderDefaultResponse()}`;
}
renderLogout() {
if (Object.keys(this.event.context).length === 0) {
return html`${msg("No additional data available.")}`;
}
return this.renderDefaultResponse();
}
renderSystemTaskException() {
return html`
${msg("Exception")}
${this.event.context.message}
`;
}
}