From a2dce3fb635266c0d2e85673664a41308b542940 Mon Sep 17 00:00:00 2001 From: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:03:00 -0800 Subject: [PATCH] web: Replace calls to `rootInterface()?.tenant?` with a contextual `this.tenant` object (#7778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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(); ``` * This commit abstracts access to the object `rootInterface()?.tenant?` into a single accessor, `tenant`, that can be mixed into any AKElement object that requires access to it. Like `WithCapabilitiesConfig` and `WithAuthentikConfig`, this one is named `WithTenantConfig`. TODO: ``` javascript rootInterface()?.uiConfig; me(); ``` * web: Added a README with a description of the applications' "mental model," essentially an architectural description. * web: prettier did a thing * web: prettier had opinions about the README * web: Jens requested that subscription be by default, and it's the right call. * web: Jens requested that the default subscription state for contexts be , and it's the right call. * web: prettier having opinions after merging with dependent branch * web: prettier still having opinions. --- ...plication-wizard-authentication-by-ldap.ts | 6 ++--- ...ication-wizard-authentication-by-radius.ts | 6 ++--- web/src/admin/groups/RelatedUserList.ts | 6 ++--- .../admin/providers/ldap/LDAPProviderForm.ts | 6 ++--- .../providers/radius/RadiusProviderForm.ts | 6 ++--- web/src/admin/users/UserListPage.ts | 5 ++-- web/src/elements/AuthentikContexts.ts | 6 ++++- web/src/elements/Interface/Interface.ts | 26 ++++++++++++++++--- .../Interface/authentikConfigProvider.ts | 2 +- web/src/elements/Interface/tenantProvider.ts | 20 ++++++++++++++ web/src/elements/PageHeader.ts | 8 +++--- web/src/elements/sidebar/SidebarBrand.ts | 11 +++----- .../details/UserSettingsFlowExecutor.ts | 15 +++++------ 13 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 web/src/elements/Interface/tenantProvider.ts diff --git a/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts index a99384171..6e1554196 100644 --- a/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts +++ b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts @@ -7,7 +7,7 @@ import "@goauthentik/components/ak-number-input"; import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-text-input"; -import { rootInterface } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; @@ -32,7 +32,7 @@ import { } from "./LDAPOptionsAndHelp"; @customElement("ak-application-wizard-authentication-by-ldap") -export class ApplicationWizardApplicationDetails extends BaseProviderPanel { +export class ApplicationWizardApplicationDetails extends WithTenantConfig(BaseProviderPanel) { render() { const provider = this.wizard.provider as LDAPProvider | undefined; const errors = this.wizard.errors.provider; @@ -57,7 +57,7 @@ export class ApplicationWizardApplicationDetails extends BaseProviderPanel {

diff --git a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts index cadbd94ad..44f452037 100644 --- a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts +++ b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts @@ -3,7 +3,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-text-input"; -import { rootInterface } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; @@ -17,7 +17,7 @@ import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/ import BaseProviderPanel from "../BaseProviderPanel"; @customElement("ak-application-wizard-authentication-by-radius") -export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel { +export class ApplicationWizardAuthenticationByRadius extends WithTenantConfig(BaseProviderPanel) { render() { const provider = this.wizard.provider as RadiusProvider | undefined; const errors = this.wizard.errors.provider; @@ -42,7 +42,7 @@ export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {

diff --git a/web/src/admin/groups/RelatedUserList.ts b/web/src/admin/groups/RelatedUserList.ts index 27450fb82..5e2c6b952 100644 --- a/web/src/admin/groups/RelatedUserList.ts +++ b/web/src/admin/groups/RelatedUserList.ts @@ -9,11 +9,11 @@ import { MessageLevel } from "@goauthentik/common/messages"; 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 { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/forms/DeleteBulkForm"; @@ -110,7 +110,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> { } @customElement("ak-user-related-list") -export class RelatedUserList extends WithCapabilitiesConfig(Table) { +export class RelatedUserList extends WithTenantConfig(WithCapabilitiesConfig(Table)) { expandable = true; checkbox = true; @@ -295,7 +295,7 @@ export class RelatedUserList extends WithCapabilitiesConfig(Table) { ${msg("Set password")} - ${rootInterface()?.tenant?.flowRecovery + ${this.tenant?.flowRecovery ? html` { +export class LDAPProviderFormPage extends WithTenantConfig(BaseProviderForm) { async loadInstance(pk: number): Promise { return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ id: pk, @@ -68,7 +68,7 @@ export class LDAPProviderFormPage extends BaseProviderForm {

${msg("Flow used for users to authenticate.")}

diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index 269a5ee95..f37c865d5 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -1,7 +1,7 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; -import { rootInterface } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/SearchSelect"; @@ -14,7 +14,7 @@ import { customElement } from "lit/decorators.js"; import { FlowsInstancesListDesignationEnum, ProvidersApi, RadiusProvider } from "@goauthentik/api"; @customElement("ak-provider-radius-form") -export class RadiusProviderFormPage extends BaseProviderForm { +export class RadiusProviderFormPage extends WithTenantConfig(BaseProviderForm) { loadInstance(pk: number): Promise { return new ProvidersApi(DEFAULT_CONFIG).providersRadiusRetrieve({ id: pk, @@ -57,7 +57,7 @@ export class RadiusProviderFormPage extends BaseProviderForm {

${msg("Flow used for users to authenticate.")}

diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts index e9d0c6f09..afb88f3f6 100644 --- a/web/src/admin/users/UserListPage.ts +++ b/web/src/admin/users/UserListPage.ts @@ -16,6 +16,7 @@ import { CapabilitiesEnum, WithCapabilitiesConfig, } from "@goauthentik/elements/Interface/capabilitiesProvider"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import { PFSize } from "@goauthentik/elements/Spinner"; import "@goauthentik/elements/TreeView"; import "@goauthentik/elements/buttons/ActionButton"; @@ -90,7 +91,7 @@ const recoveryButtonStyles = css` `; @customElement("ak-user-list") -export class UserListPage extends WithCapabilitiesConfig(TablePage) { +export class UserListPage extends WithTenantConfig(WithCapabilitiesConfig(TablePage)) { expandable = true; checkbox = true; @@ -351,7 +352,7 @@ export class UserListPage extends WithCapabilitiesConfig(TablePage) { ${msg("Set password")} - ${rootInterface()?.tenant?.flowRecovery + ${this.tenant.flowRecovery ? html` (Symbol("authentik-config-context")); +export const authentikTenantContext = createContext( + Symbol("authentik-tenant-context"), +); + export default authentikConfigContext; diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts index 744a16095..b2470cfd2 100644 --- a/web/src/elements/Interface/Interface.ts +++ b/web/src/elements/Interface/Interface.ts @@ -1,6 +1,9 @@ import { config, tenant } from "@goauthentik/common/api/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; -import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; +import { + authentikConfigContext, + authentikTenantContext, +} from "@goauthentik/elements/AuthentikContexts"; import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; @@ -21,9 +24,6 @@ type AkInterface = HTMLElement & { }; export class Interface extends AKElement implements AkInterface { - @state() - tenant?: CurrentTenant; - @state() uiConfig?: UIConfig; @@ -45,6 +45,24 @@ export class Interface extends AKElement implements AkInterface { return this._config; } + _tenantContext = new ContextProvider(this, { + context: authentikTenantContext, + initialValue: undefined, + }); + + _tenant?: CurrentTenant; + + @state() + set tenant(c: CurrentTenant) { + this._tenant = c; + this._tenantContext.setValue(c); + this.requestUpdate(); + } + + get tenant(): CurrentTenant | undefined { + return this._tenant; + } + constructor() { super(); document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; diff --git a/web/src/elements/Interface/authentikConfigProvider.ts b/web/src/elements/Interface/authentikConfigProvider.ts index 2f2bbcf43..5b2027fd0 100644 --- a/web/src/elements/Interface/authentikConfigProvider.ts +++ b/web/src/elements/Interface/authentikConfigProvider.ts @@ -12,7 +12,7 @@ export function WithAuthentikConfig>( superclass: T, subscribe = true, ) { - class WithAkConfigProvider extends superclass { + abstract class WithAkConfigProvider extends superclass { @consume({ context: authentikConfigContext, subscribe }) public authentikConfig!: Config; } diff --git a/web/src/elements/Interface/tenantProvider.ts b/web/src/elements/Interface/tenantProvider.ts new file mode 100644 index 000000000..63d389048 --- /dev/null +++ b/web/src/elements/Interface/tenantProvider.ts @@ -0,0 +1,20 @@ +import { authentikTenantContext } from "@goauthentik/elements/AuthentikContexts"; + +import { consume } from "@lit-labs/context"; +import type { LitElement } from "lit"; + +import type { CurrentTenant } from "@goauthentik/api"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = abstract new (...args: any[]) => T; + +export function WithTenantConfig>( + superclass: T, + subscribe = true, +) { + abstract class WithTenantProvider extends superclass { + @consume({ context: authentikTenantContext, subscribe }) + public tenant!: CurrentTenant; + } + return WithTenantProvider; +} diff --git a/web/src/elements/PageHeader.ts b/web/src/elements/PageHeader.ts index fcdbbeffc..7be55996d 100644 --- a/web/src/elements/PageHeader.ts +++ b/web/src/elements/PageHeader.ts @@ -8,7 +8,8 @@ import { } from "@goauthentik/common/constants"; import { currentInterface } from "@goauthentik/common/sentry"; import { me } from "@goauthentik/common/users"; -import { AKElement, rootInterface } from "@goauthentik/elements/Base"; +import { AKElement } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { msg } from "@lit/localize"; @@ -23,7 +24,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { EventsApi } from "@goauthentik/api"; @customElement("ak-page-header") -export class PageHeader extends AKElement { +export class PageHeader extends WithTenantConfig(AKElement) { @property() icon?: string; @@ -35,9 +36,8 @@ export class PageHeader extends AKElement { @property() set header(value: string) { - const tenant = rootInterface()?.tenant; const currentIf = currentInterface(); - let title = tenant?.brandingTitle || TITLE_DEFAULT; + let title = this.tenant?.brandingTitle || TITLE_DEFAULT; if (currentIf === "admin") { title = `${msg("Admin")} - ${title}`; } diff --git a/web/src/elements/sidebar/SidebarBrand.ts b/web/src/elements/sidebar/SidebarBrand.ts index fa442b36c..b57d336f7 100644 --- a/web/src/elements/sidebar/SidebarBrand.ts +++ b/web/src/elements/sidebar/SidebarBrand.ts @@ -1,6 +1,6 @@ import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; -import { first } from "@goauthentik/common/utils"; -import { AKElement, rootInterface } from "@goauthentik/elements/Base"; +import { AKElement } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement } from "lit/decorators.js"; @@ -27,7 +27,7 @@ export const DefaultTenant: CurrentTenant = { }; @customElement("ak-sidebar-brand") -export class SidebarBrand extends AKElement { +export class SidebarBrand extends WithTenantConfig(AKElement) { static get styles(): CSSResult[] { return [ PFBase, @@ -85,10 +85,7 @@ export class SidebarBrand extends AKElement {
authentik Logo diff --git a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts index e7aa36343..f4252f58b 100644 --- a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts +++ b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts @@ -2,14 +2,15 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { MessageLevel } from "@goauthentik/common/messages"; import { refreshMe } from "@goauthentik/common/users"; -import { AKElement, rootInterface } from "@goauthentik/elements/Base"; +import { AKElement } from "@goauthentik/elements/Base"; +import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { StageHost } from "@goauthentik/flow/stages/base"; import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage"; import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; @@ -21,7 +22,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { ChallengeChoices, ChallengeTypes, - CurrentTenant, FlowChallengeResponseRequest, FlowErrorChallenge, FlowsApi, @@ -31,13 +31,13 @@ import { } from "@goauthentik/api"; @customElement("ak-user-settings-flow-executor") -export class UserSettingsFlowExecutor extends AKElement implements StageHost { +export class UserSettingsFlowExecutor + extends WithTenantConfig(AKElement, true) + implements StageHost +{ @property() flowSlug?: string; - @state() - tenant?: CurrentTenant; - private _challenge?: ChallengeTypes; @property({ attribute: false }) @@ -87,7 +87,6 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost { } firstUpdated(): void { - this.tenant = rootInterface()?.tenant; this.flowSlug = this.tenant?.flowUserSettings; if (!this.flowSlug) { return;