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`
${term}
${description}
`; } 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}
    `; } }