From dcbfe7389154af9baa1dd8771ed0e542056fd914 Mon Sep 17 00:00:00 2001
From: Ken Sternberg
<133134217+kensternberg-authentik@users.noreply.github.com>
Date: Thu, 25 Jan 2024 09:21:27 -0800
Subject: [PATCH] web: provide a context for checking the status of the
enterprise license (#8153)
* 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: [x] The translation files have been updated
---
web/src/admin/common/ak-license-notice.ts | 24 +++++++++++
.../PropertyMappingWizard.ts | 42 +++++++------------
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, 118 insertions(+), 70 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 0000000000..db8eeca1fa
--- /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 59b62c39c9..d591e56c9b 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 "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
@@ -14,25 +16,22 @@ 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 { TemplateResult, html, nothing } from "lit";
+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[] {
+ static get styles() {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
@@ -51,6 +50,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
render(): TemplateResult {
return html`
`;
@@ -86,23 +83,17 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
@customElement("ak-property-mapping-wizard")
export class PropertyMappingWizard extends AKElement {
- static get styles(): CSSResult[] {
+ static get styles() {
return [PFBase, PFButton, PFRadio];
}
@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 {
@@ -115,7 +106,6 @@ export class PropertyMappingWizard extends AKElement {
${this.mappingTypes.map((type) => {
diff --git a/web/src/admin/providers/ProviderWizard.ts b/web/src/admin/providers/ProviderWizard.ts
index 7f19b4d024..ca80f995ee 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``;
@@ -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 3a0f1dd1b8..9d2aaf9b21 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, CurrentBrand } from "@goauthentik/api";
+import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
export const authentikConfigContext = createContext(Symbol("authentik-config-context"));
+export const authentikEnterpriseContext = createContext(
+ Symbol("authentik-enterprise-context"),
+);
+
export const authentikBrandContext = createContext(Symbol("authentik-brand-context"));
export default authentikConfigContext;
diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts
index ed4e57c9ae..687a4964b5 100644
--- a/web/src/elements/Interface/Interface.ts
+++ b/web/src/elements/Interface/Interface.ts
@@ -1,8 +1,10 @@
+import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { brand, config } from "@goauthentik/common/api/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import {
authentikBrandContext,
authentikConfigContext,
+ authentikEnterpriseContext,
} from "@goauthentik/elements/AuthentikContexts";
import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
@@ -12,7 +14,8 @@ import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
-import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
+import type { Config, CurrentBrand, 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._brand;
}
+ _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)];
brand().then((brand) => (this.brand = brand));
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 0000000000..64811ed127
--- /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 09d376759e..b3360fb59a 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`
`;
@@ -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;