web: detangle components from applications (#6891)

* Web: Detangling some circular dependencies in Admin and User

Admin, User, and Flow should not dependend upon each other, at least
not in a circular way.  If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.

This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.

* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson

* web: I found these confusing to look at, so I added comments.

* web: remove admin-to-user component reference(s)

There was only one: AppIcon.  This has been moved to `components`.

Touching the LibraryApplications page triggered a cyclomatic
complexity check.  Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.

* web: remove admin from elements

This commit removes the two references from `elements` to `admin`: the list of UserEvents and a
reference to the FlowSearch type, used by the Forms manager to decide how to extract a value.
For FlowSearch, a different convention for detecting the type was implemented (instances of the
object have a unique fieldname for the value holder).  UserEvents and ObjectChangelog have been
moved to `components` as they're clearly dependent upon the API.

This defers work on removing Admin from Components, as that is (again) references going the
wrong way, but that can happen later.

* web: remove admin-to-user component reference(s) (#6856)

There was only one: AppIcon.  This has been moved to `components`.

Touching the LibraryApplications page triggered a cyclomatic
complexity check.  Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.

* This was supposed to be merged.

* web: remove `./element`⇢`./user` references

The offender here is UserDevicesList, which despite being in `elements` is only
used by the admin/user/UserViewPage.  The problem is that UserDevicesList,
despite being in `admin`, inherits from `user`, so moving it would have created
a new admin⇢user reference, and the whole point of this exercise is to get rid
of references that point "up" from the foundational pieces to the views, or
that refer to components in sibling applications.

After examining UserDevicesList, I realized that *every feature* of MFADevicesList
had been overridden: the rows, the columns, the toolbar, and the endpoint all had
custom overrides.  Nothing was left of MFADevicesList after that.   Even the
property that the web component used had been completely changed.  The only thing
they had in common was that they both inherited from `Table<Device>`.

Refactoring UserDevicesList so that it inherited directly from `Table<Device>` and
then moving it into `./admin/users` was the obvious and correct step.

Both used the same label table, so that went into the `common/labels` folder.

Along the way, I cleaned up a few minor details. Just little things, like the repeated invocation
of:

```
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorAdminMETHODDestroy({ id: device.pk });
```

This is repeated five times, once for each Method.  By creating these:

```
        const api = new AuthenticatorsApi(DEFAULT_CONFIG);
        const id = { id: device.pk };
```

The method invocation could be just `api.authenticatorsMETHODDestroy(id)`, which is easier on the
eyes.  See the MFADevicesPage for the full example.

Similarly,

```
return [
   new TableColumn(msg("Name"), ""),
   new TableColumn(msg("Type"), ""),
   new TableColumn("")
];
   ```

is more straightforward as:

```
const headers = [msg("Name"), msg("Type"), ""];
return headers.map((th) => new TableColumn(th, ""));
```

We've labeled what we're working with, and web developers ought to know that `th` is the HTML code
for `table header`.

I've had to alter what files are scanned in pre-commit mode; it doesn't handle renamed files very well,
and at the moment a file that is renamed is not scanned, as its "new" name is not straightforwardly
displayed, not even by `git porcelain`.

* web: make the table of column headers look like a table

* web: detangle `common` from `elements`.

And just like that, `common` no longer has a reference to `elements`.   I don't mind this little bit of
code duplication if it removes a cycle.  What it does point out is that there are bits of `common` that
are predicated on the presence of the browser, and that there are bits of `elements` that, if they rely
on `common`, can't be disentangled from the application as a whole.  Which seems to me that we have two
different things going on in common: things about an application, and things about elements that are
independent of the application.

I'll think about those later.

```
$ rg 'import.*@goauthentik' ./common/ | perl -ne 'm{"(@goauthentik[^"]*)"} && print "$1\n"' | sort | cut -d '/' -f1-2 | uniq | sort
@goauthentik/api
@goauthentik/common
$
```

* web: odd bug; merge-related?  Gonna investigate.

* web: build failure thanks to local cache; fixed

* web: detangle `components` from `admin`.

This was the last inappropriate reference: something from `./components` referencing something in
`./admin`, in this case the `ak-event-info` component.  Used by both Users and Admin, moving it
into `./components` was the obvious correct step.

`ak-event-info` is a lookup table relating specific events in the event log to rich, textual
representations; in the special case of model changes and email info, even more rich content is
available in a dl/dt format. I've tableized the model changes and email info renderer, and I've
extracted every event's textual representation into its own method, converting the `switch/case`
rendering statement into a `switch/case` dispatch switch. This has the virtue of isolating each
unique case and making the dispatch switch short and coherent.

The conversion was done mechanistically; I gave the refactorer (Tide, in this case) instructions to
duplicate the switch block and then convert every case into a method with a name patterned on the
`case`. Going back to the original switch block, it was easy to duplicate the pattern matching and
convert it into a dispatch switch.

And with this, there are zero cycles in the references between the different "packageable" sections
of the UI.  The only thing left to do is figure out how to redistribute `./elements` and `./components`
in a way that makes sense for each.

* Changed function name from 'emailMessageBody' to 'githubIssueMessageBody' to better reflect its usage.

* web: added comments about length and purpose of githubIssueMessageBody.

* Update web/src/common/labels.ts

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>

* Unwanted change.

---------

Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg 2023-09-14 14:51:42 -07:00 committed by GitHub
parent 55dd7013b4
commit a0d2aca61c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 497 additions and 450 deletions

View file

@ -1,9 +1,9 @@
import "@goauthentik/admin/events/EventInfo";
import { EventGeo } from "@goauthentik/admin/events/utils"; import { EventGeo } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events"; import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
import { truncate } from "@goauthentik/common/utils"; import { truncate } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-event-info";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/ModalButton";

View file

@ -1,442 +0,0 @@
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 { 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";
@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;
}
`,
];
}
getModelInfo(context: EventModel): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
return html`<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("UID")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.pk}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.name}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("App")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.app}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Model Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.model_name}</div>
</dd>
</div>
</dl>
</div>`;
}
getEmailInfo(context: EventContext): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
return html`<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Message")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.message}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Subject")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.subject}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("From")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.from_email}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("To")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${(context.to_email as string[]).map((to) => {
return html`<li>${to}</li>`;
})}
</div>
</dd>
</div>
</dl>`;
}
defaultResponse(): TemplateResult {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Context")}</div>
<div class="pf-c-card__body">
<code>${JSON.stringify(this.event?.context, null, 4)}</code>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("User")}</div>
<div class="pf-c-card__body">
<code>${JSON.stringify(this.event?.user, null, 4)}</code>
</div>
</div>
</div>`;
}
buildGitHubIssueUrl(context: EventContext): string {
const httpRequest = this.event.context.http_request as EventContext;
let title = "";
if (httpRequest) {
title = `${httpRequest?.method} ${httpRequest?.path}`;
}
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
const fullBody = `
**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**
<details>
<summary>Stacktrace from authentik</summary>
\`\`\`
${context.message as string}
\`\`\`
</details>
**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.
`;
return `https://github.com/goauthentik/authentik/issues/
new?labels=bug,from_authentik&title=${encodeURIComponent(title)}
&body=${encodeURIComponent(fullBody)}`.trim();
}
render(): TemplateResult {
if (!this.event) {
return html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`;
}
switch (this.event?.action) {
case EventActions.ModelCreated:
case EventActions.ModelUpdated:
case EventActions.ModelDeleted:
return html`
<div class="pf-c-card__title">${msg("Affected model:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(this.event.context?.model as EventModel)}
</div>
`;
case EventActions.AuthorizeApplication:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Authorized application:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(
this.event.context.authorized_application as EventModel,
)}
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Using flow")}</div>
<div class="pf-c-card__body">
<span
>${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
flowUuid: this.event.context.flow as string,
})
.then((resp) => {
return html`<a
href="#/flow/flows/${resp.results[0].slug}"
>${resp.results[0].name}</a
>`;
}),
html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`,
)}
</span>
</div>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.EmailSent:
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
<ak-expand>
<iframe srcdoc=${this.event.context.body}></iframe>
</ak-expand>`;
case EventActions.SecretView:
return html` <div class="pf-c-card__title">${msg("Secret:")}</div>
${this.getModelInfo(this.event.context.secret as EventModel)}`;
case EventActions.SystemException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__title">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${this.buildGitHubIssueUrl(this.event.context)}
>
${msg("Open issue on GitHub...")}
</a>
</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message}</pre>
</div>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PropertyMappingException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message || this.event.context.error}</pre>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Expression")}</div>
<div class="pf-c-card__body">
<code>${this.event.context.expression}</code>
</div>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PolicyException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Binding")}</div>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Request")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext)
.obj as EventModel,
)}
</li>
<li>
<span
>${msg("Context")}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext)
.context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<code
>${this.event.context.message || this.event.context.error}</code
>
</div>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PolicyExecution:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Binding")}</div>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Request")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext)
.obj as EventModel,
)}
</li>
<li>
<span
>${msg("Context")}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext)
.context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Result")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Passing")}:
${(this.event.context.result as EventContext).passing}
</li>
<li>
${msg("Messages")}:
<ul class="pf-c-list">
${(
(this.event.context.result as EventContext)
.messages as string[]
).map((msg) => {
return html`<li>${msg}</li>`;
})}
</ul>
</li>
</ul>
</div>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.ConfigurationError:
return html`<div class="pf-c-card__title">${this.event.context.message}</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.UpdateAvailable:
return html`<div class="pf-c-card__title">${msg("New version available!")}</div>
<a
target="_blank"
href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this
.event.context.new_version}"
>
${this.event.context.new_version}
</a>`;
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
case EventActions.Login:
if ("using_source" in this.event.context) {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Using source")}</div>
${this.getModelInfo(this.event.context.using_source as EventModel)}
</div>
</div>`;
}
return this.defaultResponse();
case EventActions.LoginFailed:
return html` <div class="pf-c-card__title">
${msg(str`Attempted to log in as ${this.event.context.username}`)}
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.Logout:
if (Object.keys(this.event.context).length === 0) {
return html`<span>${msg("No additional data available.")}</span>`;
}
return this.defaultResponse();
case EventActions.SystemTaskException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message}</pre>
</div>
</div>
</div>`;
default:
return this.defaultResponse();
}
}
}

View file

@ -1,9 +1,9 @@
import "@goauthentik/admin/events/EventInfo";
import { EventGeo } from "@goauthentik/admin/events/utils"; import { EventGeo } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events"; import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-event-info";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";

View file

@ -1,8 +1,8 @@
import "@goauthentik/admin/events/EventInfo";
import { EventGeo } from "@goauthentik/admin/events/utils"; import { EventGeo } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events"; import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
import "@goauthentik/components/ak-event-info";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader"; import "@goauthentik/elements/PageHeader";

View file

@ -0,0 +1,491 @@
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**
<details>
<summary>Stacktrace from authentik</summary>
\`\`\`
${context.message as string}
\`\`\`
</details>
**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` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${term}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${description}</div>
</dd>
</div>`;
}
getModelInfo(context: EventModel): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
const modelFields: Pair[] = [
[msg("UID"), context.pk],
[msg("Name"), context.name],
[msg("App"), context.app],
[msg("Model Name"), context.model_name],
];
return html`<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-horizontal">
${map(modelFields, this.renderDescriptionGroup)}
</dl>
</div>`;
}
getEmailInfo(context: EventContext): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
// 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`<li>${to}</li>`;
})}`],
];
return html`<dl class="pf-c-description-list pf-m-horizontal">
${map(emailFields, this.renderDescriptionGroup)}
</dl>`;
}
renderDefaultResponse(): TemplateResult {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Context")}</div>
<div class="pf-c-card__body">
<code>${JSON.stringify(this.event?.context, null, 4)}</code>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("User")}</div>
<div class="pf-c-card__body">
<code>${JSON.stringify(this.event?.user, null, 4)}</code>
</div>
</div>
</div>`;
}
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`<ak-spinner size=${PFSize.Medium}></ak-spinner>`;
}
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`
<div class="pf-c-card__title">${msg("Affected model:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(this.event.context?.model as EventModel)}
</div>
`;
}
renderAuthorizeApplication() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Authorized application:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(
this.event.context.authorized_application as EventModel,
)}
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Using flow")}</div>
<div class="pf-c-card__body">
<span
>${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
flowUuid: this.event.context.flow as string,
})
.then((resp) => {
return html`<a href="#/flow/flows/${resp.results[0].slug}"
>${resp.results[0].name}</a
>`;
}),
html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`,
)}
</span>
</div>
</div>
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderEmailSent() {
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
<ak-expand>
<iframe srcdoc=${this.event.context.body}></iframe>
</ak-expand>`;
}
renderSecretView() {
return html` <div class="pf-c-card__title">${msg("Secret:")}</div>
${this.getModelInfo(this.event.context.secret as EventModel)}`;
}
renderSystemException() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__title">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${this.buildGitHubIssueUrl(this.event.context)}
>
${msg("Open issue on GitHub...")}
</a>
</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message}</pre>
</div>
</div>
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderPropertyMappingException() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message || this.event.context.error}</pre>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Expression")}</div>
<div class="pf-c-card__body">
<code>${this.event.context.expression}</code>
</div>
</div>
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderPolicyException() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Binding")}</div>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Request")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext).obj as EventModel,
)}
</li>
<li>
<span
>${msg("Context")}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext).context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<code>${this.event.context.message || this.event.context.error}</code>
</div>
</div>
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderPolicyExecution() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Binding")}</div>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Request")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Object")}:
${this.getModelInfo(
(this.event.context.request as EventContext).obj as EventModel,
)}
</li>
<li>
<span
>${msg("Context")}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext).context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
</div>
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Result")}</div>
<div class="pf-c-card__body">
<ul class="pf-c-list">
<li>
${msg("Passing")}:
${(this.event.context.result as EventContext).passing}
</li>
<li>
${msg("Messages")}:
<ul class="pf-c-list">
${(
(this.event.context.result as EventContext)
.messages as string[]
).map((msg) => {
return html`<li>${msg}</li>`;
})}
</ul>
</li>
</ul>
</div>
</div>
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderConfigurationError() {
return html`<div class="pf-c-card__title">${this.event.context.message}</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderUpdateAvailable() {
return html`<div class="pf-c-card__title">${msg("New version available")}</div>
<a
target="_blank"
href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this.event
.context.new_version}"
>
${this.event.context.new_version}
</a>`;
// 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`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Using source")}</div>
${this.getModelInfo(this.event.context.using_source as EventModel)}
</div>
</div>`;
}
return this.renderDefaultResponse();
}
renderLoginFailed() {
return html` <div class="pf-c-card__title">
${msg(str`Attempted to log in as ${this.event.context.username}`)}
</div>
<ak-expand>${this.renderDefaultResponse()}</ak-expand>`;
}
renderLogout() {
if (Object.keys(this.event.context).length === 0) {
return html`<span>${msg("No additional data available.")}</span>`;
}
return this.renderDefaultResponse();
}
renderSystemTaskException() {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Exception")}</div>
<div class="pf-c-card__body">
<pre>${this.event.context.message}</pre>
</div>
</div>
</div>`;
}
}

View file

@ -1,7 +1,7 @@
import "@goauthentik/admin/events/EventInfo";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events"; import { EventWithContext } from "@goauthentik/common/events";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-event-info";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/ModalButton";

View file

@ -1,15 +1,13 @@
import "@goauthentik/admin/events/EventInfo";
import "@goauthentik/admin/events/EventInfo";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events"; import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-event-info";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";