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:
		@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user