From b36e057a9266530c409d2415a549cb90cf5f6e71 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 12 Jan 2024 14:21:16 -0800 Subject: [PATCH] web: provide a context for enterprise license status There are a few places (currently 5) in our code where we have checks for the current enterprise licensing status of our product. While not particularly heavy or onerous, there's no reason to repeat those same lines, and since our UI is always running in the context of authentik, may as well make that status a client-side context in its own right. The status will update with an EVENT_REFRESH request. A context-aware custom alert has also been provided; it draws itself (or `nothing`) depending on the state of the license, and the default message, "This feature requires an enterprise license," can be overriden with the `notice` property. These two changes reduce the amount of code needed to manage our license alerting from 67 to 38 lines code, and while removing 29 lines from a product with 54,145 lines of code (a savings of 0.05%, oh boy!) isn't a miracle, it does mean there's a single source of truth for "Is this instance enterprise-licensed?" that's easy to access and use. --- web/src/admin/common/ak-license-notice.ts | 24 +++++++++++ .../PropertyMappingWizard.ts | 35 ++++++---------- web/src/admin/providers/ProviderWizard.ts | 41 +++++++------------ web/src/elements/AuthentikContexts.ts | 6 ++- web/src/elements/Interface/Interface.ts | 27 +++++++++++- .../Interface/licenseSummaryProvider.ts | 25 +++++++++++ .../enterprise/EnterpriseStatusBanner.ts | 23 ++++------- 7 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 web/src/admin/common/ak-license-notice.ts create mode 100644 web/src/elements/Interface/licenseSummaryProvider.ts diff --git a/web/src/admin/common/ak-license-notice.ts b/web/src/admin/common/ak-license-notice.ts new file mode 100644 index 000000000..db8eeca1f --- /dev/null +++ b/web/src/admin/common/ak-license-notice.ts @@ -0,0 +1,24 @@ +import "@goauthentik/elements/Alert"; +import { AKElement } from "@goauthentik/elements/Base"; +import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; + +import { msg } from "@lit/localize"; +import { html, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-license-notice") +export class AkLicenceNotice extends WithLicenseSummary(AKElement) { + @property() + notice = msg("This feature requires an enterprise license."); + + render() { + return this.hasEnterpriseLicense + ? nothing + : html` + + ${this.notice} + ${msg("Learn more")} + + `; + } +} diff --git a/web/src/admin/property-mappings/PropertyMappingWizard.ts b/web/src/admin/property-mappings/PropertyMappingWizard.ts index 4f0ab6122..15dc6047a 100644 --- a/web/src/admin/property-mappings/PropertyMappingWizard.ts +++ b/web/src/admin/property-mappings/PropertyMappingWizard.ts @@ -1,9 +1,11 @@ +import "@goauthentik/admin/common/ak-license-notice"; import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; import "@goauthentik/admin/property-mappings/PropertyMappingRACForm"; import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; +import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/forms/ProxyForm"; @@ -14,23 +16,20 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; import { msg, str } from "@lit/localize"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { CSSResult, TemplateResult, html, nothing } from "lit"; -import { property, state } from "lit/decorators.js"; +import { property } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { EnterpriseApi, LicenseSummary, PropertymappingsApi, TypeCreate } from "@goauthentik/api"; +import { PropertymappingsApi, TypeCreate } from "@goauthentik/api"; @customElement("ak-property-mapping-wizard-initial") -export class InitialPropertyMappingWizardPage extends WizardPage { +export class InitialPropertyMappingWizardPage extends WithLicenseSummary(WizardPage) { @property({ attribute: false }) mappingTypes: TypeCreate[] = []; - @property({ attribute: false }) - enterprise?: LicenseSummary; - static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, PFRadio]; } @@ -50,6 +49,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage { render(): TemplateResult { return html`
${this.mappingTypes.map((type) => { + const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense; return html`
- ${type.description} - ${type.requiresEnterprise && !this.enterprise?.hasLicense - ? html` - - ${msg("Provider require enterprise.")} - ${msg("Learn more")} - - ` - : nothing} + ${type.description} + ${requiresEnteprise + ? html`` + : nothing}
`; })}
`; @@ -92,16 +89,10 @@ export class PropertyMappingWizard extends AKElement { @property({ attribute: false }) mappingTypes: TypeCreate[] = []; - @state() - enterprise?: LicenseSummary; - async firstUpdated(): Promise { this.mappingTypes = await new PropertymappingsApi( DEFAULT_CONFIG, ).propertymappingsAllTypesList(); - this.enterprise = await new EnterpriseApi( - DEFAULT_CONFIG, - ).enterpriseLicenseSummaryRetrieve(); } render(): TemplateResult { diff --git a/web/src/admin/providers/ProviderWizard.ts b/web/src/admin/providers/ProviderWizard.ts index 7f19b4d02..ca80f995e 100644 --- a/web/src/admin/providers/ProviderWizard.ts +++ b/web/src/admin/providers/ProviderWizard.ts @@ -1,8 +1,10 @@ +import "@goauthentik/admin/common/ak-license-notice"; import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderImportForm"; +import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/Alert"; import { AKElement } from "@goauthentik/elements/Base"; @@ -15,7 +17,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; import { msg, str } from "@lit/localize"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { CSSResult, TemplateResult, html, nothing } from "lit"; -import { property, state } from "lit/decorators.js"; +import { property } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; @@ -23,16 +25,13 @@ import PFHint from "@patternfly/patternfly/components/Hint/hint.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { EnterpriseApi, LicenseSummary, ProvidersApi, TypeCreate } from "@goauthentik/api"; +import { ProvidersApi, TypeCreate } from "@goauthentik/api"; @customElement("ak-provider-wizard-initial") -export class InitialProviderWizardPage extends WizardPage { +export class InitialProviderWizardPage extends WithLicenseSummary(WizardPage) { @property({ attribute: false }) providerTypes: TypeCreate[] = []; - @property({ attribute: false }) - enterprise?: LicenseSummary; - static get styles(): CSSResult[] { return [PFBase, PFForm, PFHint, PFButton, PFRadio]; } @@ -73,6 +72,7 @@ export class InitialProviderWizardPage extends WizardPage { render(): TemplateResult { return html`
${this.providerTypes.map((type) => { + const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; return html`
- ${type.description} - ${type.requiresEnterprise && !this.enterprise?.hasLicense - ? html` - - ${msg("Provider require enterprise.")} - ${msg("Learn more")} - - ` - : nothing} + ${type.description} + ${requiresEnterprise + ? html`` + : nothing} +
`; })}
`; @@ -113,9 +110,6 @@ export class ProviderWizard extends AKElement { @property({ attribute: false }) providerTypes: TypeCreate[] = []; - @state() - enterprise?: LicenseSummary; - @property({ attribute: false }) finalHandler: () => Promise = () => { return Promise.resolve(); @@ -123,9 +117,6 @@ export class ProviderWizard extends AKElement { async firstUpdated(): Promise { this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); - this.enterprise = await new EnterpriseApi( - DEFAULT_CONFIG, - ).enterpriseLicenseSummaryRetrieve(); } render(): TemplateResult { @@ -138,11 +129,7 @@ export class ProviderWizard extends AKElement { return this.finalHandler(); }} > - + ${this.providerTypes.map((type) => { return html` diff --git a/web/src/elements/AuthentikContexts.ts b/web/src/elements/AuthentikContexts.ts index 02fa89316..7e3a1e78b 100644 --- a/web/src/elements/AuthentikContexts.ts +++ b/web/src/elements/AuthentikContexts.ts @@ -1,9 +1,13 @@ import { createContext } from "@lit-labs/context"; -import type { Config, CurrentTenant } from "@goauthentik/api"; +import type { Config, CurrentTenant, LicenseSummary } from "@goauthentik/api"; export const authentikConfigContext = createContext(Symbol("authentik-config-context")); +export const authentikEnterpriseContext = createContext( + Symbol("authentik-enterprise-context"), +); + export const authentikTenantContext = createContext( Symbol("authentik-tenant-context"), ); diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts index b2470cfd2..5cdf7a082 100644 --- a/web/src/elements/Interface/Interface.ts +++ b/web/src/elements/Interface/Interface.ts @@ -1,7 +1,9 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { config, tenant } from "@goauthentik/common/api/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { authentikConfigContext, + authentikEnterpriseContext, authentikTenantContext, } from "@goauthentik/elements/AuthentikContexts"; import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types"; @@ -12,7 +14,8 @@ import { state } from "lit/decorators.js"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api"; +import type { Config, CurrentTenant, LicenseSummary } from "@goauthentik/api"; +import { EnterpriseApi, UiThemeEnum } from "@goauthentik/api"; import { AKElement } from "../Base"; @@ -63,11 +66,33 @@ export class Interface extends AKElement implements AkInterface { return this._tenant; } + _licenseSummaryContext = new ContextProvider(this, { + context: authentikEnterpriseContext, + initialValue: undefined, + }); + + _licenseSummary?: LicenseSummary; + + @state() + set licenseSummary(c: LicenseSummary) { + this._licenseSummary = c; + this._licenseSummaryContext.setValue(c); + this.requestUpdate(); + } + + get licenseSummary(): LicenseSummary | undefined { + return this._licenseSummary; + } + constructor() { super(); document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; tenant().then((tenant) => (this.tenant = tenant)); config().then((config) => (this.config = config)); + new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => { + this.licenseSummary = enterprise; + }); + this.dataset.akInterfaceRoot = "true"; } diff --git a/web/src/elements/Interface/licenseSummaryProvider.ts b/web/src/elements/Interface/licenseSummaryProvider.ts new file mode 100644 index 000000000..64811ed12 --- /dev/null +++ b/web/src/elements/Interface/licenseSummaryProvider.ts @@ -0,0 +1,25 @@ +import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; + +import { consume } from "@lit-labs/context"; +import type { LitElement } from "lit"; + +import type { LicenseSummary } from "@goauthentik/api"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = abstract new (...args: any[]) => T; + +export function WithLicenseSummary>( + superclass: T, + subscribe = true, +) { + abstract class WithEnterpriseProvider extends superclass { + @consume({ context: authentikEnterpriseContext, subscribe }) + public licenseSummary!: LicenseSummary; + + get hasEnterpriseLicense() { + return this.licenseSummary?.hasLicense; + } + } + + return WithEnterpriseProvider; +} diff --git a/web/src/elements/enterprise/EnterpriseStatusBanner.ts b/web/src/elements/enterprise/EnterpriseStatusBanner.ts index 09d376759..b3360fb59 100644 --- a/web/src/elements/enterprise/EnterpriseStatusBanner.ts +++ b/web/src/elements/enterprise/EnterpriseStatusBanner.ts @@ -1,19 +1,14 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; +import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; 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 PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; -import { EnterpriseApi, LicenseSummary } from "@goauthentik/api"; - @customElement("ak-enterprise-status") -export class EnterpriseStatusBanner extends AKElement { - @state() - summary?: LicenseSummary; - +export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) { @property() interface: "admin" | "user" | "" = ""; @@ -21,12 +16,10 @@ export class EnterpriseStatusBanner extends AKElement { return [PFBanner]; } - async firstUpdated(): Promise { - this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve(); - } - renderBanner(): TemplateResult { - return html`
+ return html`
${msg("Warning: The current user count has exceeded the configured licenses.")} ${msg("Click here for more info.")}
`; @@ -35,12 +28,12 @@ export class EnterpriseStatusBanner extends AKElement { render(): TemplateResult { switch (this.interface.toLowerCase()) { case "admin": - if (this.summary?.showAdminWarning || this.summary?.readOnly) { + if (this.licenseSummary?.showAdminWarning || this.licenseSummary?.readOnly) { return this.renderBanner(); } break; case "user": - if (this.summary?.showUserWarning || this.summary?.readOnly) { + if (this.licenseSummary?.showUserWarning || this.licenseSummary?.readOnly) { return this.renderBanner(); } break;