* 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.
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
|
|
|
import { consume } from "@lit-labs/context";
|
|
import type { LitElement } from "lit";
|
|
|
|
import { CapabilitiesEnum } from "@goauthentik/api";
|
|
import { Config } from "@goauthentik/api";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type Constructor<T = object> = abstract new (...args: any[]) => T;
|
|
|
|
// Using a unique, lexically scoped, and locally static symbol as the field name for the context
|
|
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
|
|
// guarantees in JavaScript.
|
|
|
|
class WCC {
|
|
public static readonly capabilitiesConfig: unique symbol = Symbol();
|
|
}
|
|
|
|
/**
|
|
* withCapabilitiesContext mixes in a single method to any LitElement, `can()`, which takes a
|
|
* CapabilitiesEnum and returns true or false.
|
|
*
|
|
* Usage:
|
|
*
|
|
* After importing, simply mixin this function:
|
|
*
|
|
* ```
|
|
* export class AkMyNiftyNewFeature extends withCapabilitiesContext(AKElement) {
|
|
* ```
|
|
*
|
|
* And then if you need to check on a capability:
|
|
*
|
|
* ```
|
|
* if (this.can(CapabilitiesEnum.IsEnterprise) { ... }
|
|
* ```
|
|
*
|
|
* This code re-exports CapabilitiesEnum, so you won't have to import it on a separate line if you
|
|
* don't need anything else from the API.
|
|
*
|
|
* Passing `true` as the second mixin argument will cause the inheriting class to subscribe to the
|
|
* configuration context. Should the context be explicitly reset, all active web components that are
|
|
* currently active and subscribed to the context will automatically have a `requestUpdate()`
|
|
* triggered with the new configuration.
|
|
*
|
|
*/
|
|
|
|
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>(
|
|
superclass: T,
|
|
subscribe = true,
|
|
) {
|
|
abstract class CapabilitiesContext extends superclass {
|
|
@consume({ context: authentikConfigContext, subscribe })
|
|
private [WCC.capabilitiesConfig]!: Config;
|
|
|
|
can(c: CapabilitiesEnum) {
|
|
if (!this[WCC.capabilitiesConfig]) {
|
|
throw new Error(
|
|
"ConfigContext: Attempted to access site configuration before initialization.",
|
|
);
|
|
}
|
|
return this[WCC.capabilitiesConfig].capabilities.includes(c);
|
|
}
|
|
}
|
|
|
|
return CapabilitiesContext;
|
|
}
|
|
|
|
export { CapabilitiesEnum };
|