web: abstract rootInterface()?.config?.capabilities.includes() into .can() (#7737)

* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.

Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:

``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```

into:

``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```

It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.

The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:

    Symbol(): {
        cacheTimeout: 300
        cacheTimeoutFlows: 300
        cacheTimeoutPolicies: 300
        cacheTimeoutReputation: 300
        capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
    }

Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.

The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.

The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.

Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:

TODO:

``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```

* web: Added a README with a description of the applications' "mental model," essentially an architectural description.

* web: prettier had opinions about the README

* web: Jens requested that subscription be  by default, and it's the right call.

* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.

Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:

``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```

into:

``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```

It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.

The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:

    Symbol(): {
        cacheTimeout: 300
        cacheTimeoutFlows: 300
        cacheTimeoutPolicies: 300
        cacheTimeoutReputation: 300
        capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
    }

Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.

The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.

The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.

Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:

TODO:

``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```

* web: Added a README with a description of the applications' "mental model," essentially an architectural description.

* web: prettier had opinions about the README

* web: Jens requested that subscription be  by default, and it's the right call.

* web: adjust RAC to point to the (now independent) Interface.

- Also, removed redundant check.
This commit is contained in:
Ken Sternberg
2024-01-08 10:22:52 -08:00
committed by GitHub
parent c9dc500a2b
commit d555c0db41
27 changed files with 343 additions and 151 deletions

View File

@ -7,7 +7,7 @@ import {
import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
import "@goauthentik/elements/messages/MessageContainer";

View File

@ -1,23 +1,25 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { AKElement } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers";
import { consume } from "@lit-labs/context";
import { msg, str } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { AdminApi, CapabilitiesEnum, CoreApi, UiThemeEnum, Version } from "@goauthentik/api";
import type { Config, SessionUser, UserSelf } from "@goauthentik/api";
import { AdminApi, CoreApi, UiThemeEnum, Version } from "@goauthentik/api";
import type { SessionUser, UserSelf } from "@goauthentik/api";
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends AKElement {
export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
@property({ type: Boolean, reflect: true })
open = true;
@ -27,9 +29,6 @@ export class AkAdminSidebar extends AKElement {
@state()
impersonation: UserSelf["username"] | null = null;
@consume({ context: authentikConfigContext })
public config!: Config;
constructor() {
super();
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
@ -200,7 +199,7 @@ export class AkAdminSidebar extends AKElement {
}
renderEnterpriseMessage() {
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
return this.can(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>

View File

@ -74,10 +74,7 @@ export class AdminOverviewPage extends AKElement {
}
render(): TemplateResult {
let name = this.user?.user.username;
if (this.user?.user.name) {
name = this.user.user.name;
}
const name = this.user?.user.name ?? this.user?.user.username;
return html`<ak-page-header icon="" header="" description=${msg("General system status")}>
<span slot="header"> ${msg(str`Welcome, ${name}.`)} </span>
</ak-page-header>

View File

@ -1,13 +1,16 @@
import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-file-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
@ -22,13 +25,7 @@ import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
Application,
CapabilitiesEnum,
CoreApi,
PolicyEngineMode,
Provider,
} from "@goauthentik/api";
import { Application, CoreApi, PolicyEngineMode, Provider } from "@goauthentik/api";
import "./components/ak-backchannel-input";
import "./components/ak-provider-search-input";
@ -48,7 +45,7 @@ export const policyOptions = [
];
@customElement("ak-application-form")
export class ApplicationForm extends ModelForm<Application, string> {
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
constructor() {
super();
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
@ -93,8 +90,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
applicationRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"];
if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
@ -140,21 +136,21 @@ export class ApplicationForm extends ModelForm<Application, string> {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-text-input
name="name"
.value=${this.instance?.name}
value=${ifDefined(this.instance?.name)}
label=${msg("Name")}
required
help=${msg("Application's display Name.")}
></ak-text-input>
<ak-text-input
name="slug"
.value=${this.instance?.slug}
value=${ifDefined(this.instance?.slug)}
label=${msg("Slug")}
required
help=${msg("Internal application name used in URLs.")}
></ak-text-input>
<ak-text-input
name="group"
.value=${this.instance?.group}
value=${ifDefined(this.instance?.group)}
label=${msg("Group")}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
@ -163,7 +159,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
<ak-provider-search-input
name="provider"
label=${msg("Provider")}
.value=${this.instance?.provider}
value=${ifDefined(this.instance?.provider ?? undefined)}
help=${msg("Select a provider that this application should use.")}
blankable
></ak-provider-search-input>
@ -209,11 +205,11 @@ export class ApplicationForm extends ModelForm<Application, string> {
)}
>
</ak-switch-input>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-file-input
label="${msg("Icon")}"
name="metaIcon"
.value=${this.instance?.metaIcon}
value=${ifDefined(this.instance?.metaIcon ?? undefined)}
current=${msg("Currently set to:")}
></ak-file-input>
${this.instance?.metaIcon

View File

@ -1,8 +1,11 @@
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -14,7 +17,6 @@ import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CapabilitiesEnum,
DeniedActionEnum,
Flow,
FlowDesignationEnum,
@ -24,7 +26,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-flow-form")
export class FlowForm extends ModelForm<Flow, string> {
export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
async loadInstance(pk: string): Promise<Flow> {
const flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({
slug: pk,
@ -54,8 +56,8 @@ export class FlowForm extends ModelForm<Flow, string> {
flowRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["background"];
if (icon || this.clearBackground) {
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
@ -340,7 +342,7 @@ export class FlowForm extends ModelForm<Flow, string> {
</option>
</select>
</ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal
label=${msg("Background")}
name="background"

View File

@ -118,7 +118,7 @@ export class GroupViewPage extends AKElement {
<div class="pf-c-description-list__text">
<ak-status-label
type="warning"
?good=${this.group.isSuperuser}
?good${this.group.isSuperuser}
></ak-status-label>
</div>
</dd>

View File

@ -10,6 +10,10 @@ import { uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/forms/DeleteBulkForm";
@ -33,7 +37,6 @@ import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import {
CapabilitiesEnum,
CoreApi,
CoreUsersListTypeEnum,
Group,
@ -107,7 +110,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
}
@customElement("ak-user-related-list")
export class RelatedUserList extends Table<User> {
export class RelatedUserList extends WithCapabilitiesConfig(Table<User>) {
expandable = true;
checkbox = true;
@ -188,8 +191,7 @@ export class RelatedUserList extends Table<User> {
row(item: User): TemplateResult[] {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
item.pk !== this.me?.user.pk;
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div>

View File

@ -4,9 +4,12 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -17,7 +20,6 @@ import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CapabilitiesEnum,
FlowsInstancesListDesignationEnum,
OAuthSource,
OAuthSourceRequest,
@ -28,7 +30,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-source-oauth-form")
export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuthSource>) {
async loadInstance(pk: string): Promise<OAuthSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({
slug: pk,
@ -318,7 +320,7 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon

View File

@ -2,10 +2,13 @@ import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -16,7 +19,6 @@ import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CapabilitiesEnum,
FlowsInstancesListDesignationEnum,
PlexSource,
SourcesApi,
@ -24,7 +26,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-source-plex-form")
export class PlexSourceForm extends BaseSourceForm<PlexSource> {
export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSource>) {
async loadInstance(pk: string): Promise<PlexSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexRetrieve({
slug: pk,
@ -63,8 +65,7 @@ export class PlexSourceForm extends BaseSourceForm<PlexSource> {
plexSourceRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
@ -255,7 +256,7 @@ export class PlexSourceForm extends BaseSourceForm<PlexSource> {
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon

View File

@ -5,7 +5,10 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -18,7 +21,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
import {
BindingTypeEnum,
CapabilitiesEnum,
DigestAlgorithmEnum,
FlowsInstancesListDesignationEnum,
NameIdPolicyEnum,
@ -29,7 +31,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-source-saml-form")
export class SAMLSourceForm extends BaseSourceForm<SAMLSource> {
export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSource>) {
@state()
clearIcon = false;
@ -149,7 +151,7 @@ export class SAMLSourceForm extends BaseSourceForm<SAMLSource> {
</option>
</select>
</ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon

View File

@ -12,6 +12,10 @@ import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/TreeView";
import "@goauthentik/elements/buttons/ActionButton";
@ -33,14 +37,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import {
CapabilitiesEnum,
CoreApi,
ResponseError,
SessionUser,
User,
UserPath,
} from "@goauthentik/api";
import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
export const requestRecoveryLink = (user: User) =>
new CoreApi(DEFAULT_CONFIG)
@ -93,7 +90,7 @@ const recoveryButtonStyles = css`
`;
@customElement("ak-user-list")
export class UserListPage extends TablePage<User> {
export class UserListPage extends WithCapabilitiesConfig(TablePage<User>) {
expandable = true;
checkbox = true;
@ -244,8 +241,7 @@ export class UserListPage extends TablePage<User> {
row(item: User): TemplateResult[] {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
item.pk !== this.me?.user.pk;
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div>

View File

@ -22,8 +22,9 @@ import {
import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/components/events/UserEvents";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/PageHeader";
import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/Tabs";
@ -60,7 +61,7 @@ import {
import "./UserDevicesTable";
@customElement("ak-user-view")
export class UserViewPage extends AKElement {
export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
@property({ type: Number })
set userId(id: number) {
me().then((me) => {
@ -163,8 +164,7 @@ export class UserViewPage extends AKElement {
renderActionButtons(user: User) {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
user.pk !== this.me?.user.pk;
this.can(CapabilitiesEnum.CanImpersonate) && user.pk !== this.me?.user.pk;
return html`<div class="ak-button-collection">
<ak-forms-modal>