scimmy
Version:
SCIMMY - SCIM m(ade eas)y
228 lines (214 loc) • 11.2 kB
JavaScript
import './types.js';
import { ServiceProviderConfig } from './schemas.js';
// Define handler traps for returned proxied configuration object
const catchAll = () => {throw new TypeError("SCIM Configuration can only be changed via the 'set' method")};
const handleTraps = {set: catchAll, deleteProperty: catchAll, defineProperty: catchAll};
/**
* SCIMMY Service Provider Configuration Class
* @module scimmy/config
* @namespace SCIMMY.Config
* @description
* SCIMMY provides a singleton class, `SCIMMY.Config`, that acts as a central store for a SCIM Service Provider's configuration.
* It is used for defining SCIM specification features supported (e.g. PATCH, sort, filter, etc).
* This can be either directly by an implementing service provider, or retrieved by a client (identity provider) from a remote service provider.
* By default, all specification features are marked as disabled, as your implementation may not support them.
*
* ## Retrieving Configuration
* The stored configuration can be retrieved by calling `{@link SCIMMY.Config.get}()`, which returns a cloned object
* representing the configuration _at the time of retrieval_.
*
* > **Note:**
* > To prevent accidental configuration changes, the returned object has been trapped, and attempting to change a configuration
* > value directly on this object will throw a TypeError with the message `"SCIM Configuration can only be changed via the 'set' method"`
*
* The structure of the object reflects the example provided in [RFC7643§8.5](https://datatracker.ietf.org/doc/html/rfc7643#section-8.5):
* ```json
* {
* "documentationUri": "/path/to/documentation.html",
* "patch": {
* "supported": false
* },
* "bulk": {
* "supported": false,
* "maxOperations": 1000,
* "maxPayloadSize": 1048576
* },
* "filter": {
* "supported": false,
* "maxResults": 200
* },
* "changePassword": {
* "supported": false
* },
* "sort": {
* "supported": false
* },
* "etag": {
* "supported": false
* },
* "authenticationSchemes": []
* }
* ```
*
* ## Setting Configuration
* The stored configuration can be changed via the `{@link SCIMMY.Config.set}` method. This method can be called either with an object representing the new configuration, or with a configuration property name string and value pair.
* * Where the only child property of a top-level configuration property is "supported", a boolean can be supplied as the value, which will be used as the value of the "supported" property.
* ```js
* // This will set patch.supported to true
* SCIMMY.Config.set("patch", true);
* ```
* * The "filter" and "bulk" properties also accept a number value, which will be interpreted as being the value of the "maxResults" and "maxOperations" child properties respectively, and will automatically set "supported" to true.
* ```js
* // This will set filter.maxResults to 20, and filter.supported to true
* SCIMMY.Config.set("filter", 20);
* ```
*
* > **Note:**
* > Supplied values are validated against SCIMMY's ServiceProviderConfig schema definition.
* > Providing values with incompatible types (e.g. the string "100" instead of the number 100) will throw a TypeError.
* > This ensures configuration values always conform to the standard. See [RFC7643§5](https://datatracker.ietf.org/doc/html/rfc7643#section-5) for more information.
*
* Multiple values can also be set at the same time, and changes are cumulative, so omitted properties will not be unset:
* ```js
* // With both shorthand and full syntax
* SCIMMY.Config.set({
* documentationUri: "https://example.com/docs/scim.html",
* patch: true,
* filter: 100,
* bulk: {
* supported: true,
* maxPayloadSize: 2097152
* },
* authenticationSchemes: [
* {/ Your authentication scheme details /}
* ]
* });
* ```
*
* ### Authentication Schemes
* Service provider authentication schemes can be set in the same way as other configuration properties, and are cumulative.
* The authenticationSchemes collection can be reset by providing an empty array as the value for the authenticationSchemes property.
* ```js
* // Both of these will append the supplied values to the authenticationSchemes property
* SCIMMY.Config.set("authenticationSchemes", {/ Your authentication scheme details /});
* SCIMMY.Config.set("authenticationSchemes", [
* {/ Your primary authentication scheme details /},
* {/ Your secondary authentication scheme details /}
* ]);
*
* // Reset the authenticationSchemes collection
* SCIMMY.Config.set("authenticationSchemes", []);
* ```
*/
class Config {
/**
* Store the configuration
* @private
*/
static #config = {
documentationUri: undefined,
patch: Object.preventExtensions({supported: false}),
bulk: Object.preventExtensions({supported: false, maxOperations: 1000, maxPayloadSize: 1048576}),
filter: Object.preventExtensions({supported: false, maxResults: 200}),
changePassword: Object.preventExtensions({supported: false}),
sort: Object.preventExtensions({supported: false}),
etag: Object.preventExtensions({supported: false}),
authenticationSchemes: []
};
/**
* Get SCIM service provider configuration
* @returns {Object} the service provider configuration, proxied for protection
*/
static get() {
// Wrap all the things in a proxy!
return new Proxy(Object.entries(Config.#config).reduce((res, [key, value]) => Object.assign(res, {
[key]: (key === "documentationUri" ? value : new Proxy(value, handleTraps))
}), {}), handleTraps);
}
/**
* Set multiple SCIM service provider configuration property values
* @overload
* @param {Object} config - the new configuration to apply to the service provider config instance
* @returns {Object} the updated configuration instance
*/
/**
* Set specific SCIM service provider configuration property by name
* @overload
* @param {String} name - the name of the configuration property to set
* @param {Object|Boolean} value - the new value of the configuration property to set
* @returns {typeof SCIMMY.Config} the config container class for chaining
*/
/**
* Set SCIM service provider configuration
* @param {Object|String} name - the configuration key name or value to apply
* @param {Object|String|Boolean} [config=name] - the new configuration to apply to the service provider config instance
* @returns {Object|typeof SCIMMY.Config} the updated configuration instance, or the config container class for chaining
*/
static set(name, config = name) {
// If property name supplied, call again with object
if (typeof name === "string") {
Config.set({[name]: config});
return Config;
}
// If name was omitted, assume config is for top-level assignment
else if (config === name && config === Object(config)) {
// Make sure all property names are valid
for (let key of Object.keys(config)) {
if (!(key in Config.#config))
throw new TypeError(`SCIM configuration: schema does not define attribute '${key}'`);
}
// They must be valid, so apply them to the config
for (let [key, value] of Object.entries(config)) {
let target = Config.#config[key];
if (key === "documentationUri") {
// documentationUri must be a string
if (!!value && String(value) !== value)
throw new TypeError("SCIM configuration: attribute 'documentationUri' expected value type 'string'");
// Assign documentationUri string
if (!!value) Config.#config.documentationUri = ServiceProviderConfig.definition.attribute(key).coerce(value);
else Config.#config.documentationUri = undefined;
} else if (Array.isArray(target)) {
// Target is multi-valued (authenticationSchemes), add coerced values to config, or reset if empty
if (!value || (Array.isArray(value) && value.length === 0)) target.splice(0);
else target.push(...ServiceProviderConfig.definition.attribute(key).coerce(Array.isArray(value) ? value : [value]));
} else {
// Strings are not valid shorthand config values
if (typeof value === "string")
throw new TypeError(`SCIM configuration: attribute '${key}' expected value type 'complex' but got 'string'`);
// Booleans are valid shorthand for {supported: true}
else if (typeof value === "boolean")
target.supported = value;
// Numbers are valid shorthand for filter (maxResults) and bulk (maxOperations) config values
else if (typeof value === "number") {
// Expect key to be bulk or filter
if (!["bulk", "filter"].includes(key))
throw new TypeError(`SCIM configuration: property '${key}' does not define any number-based attributes`);
// Expect number to not be negative
if (value < 0)
throw new TypeError(`SCIM configuration: property '${key}' expects number value to be zero or more`);
// Toggle support and assign relevant config value
target.supported = (!!value && value >= 0);
target[key === "filter" ? "maxResults" : "maxOperations"] = value;
}
// No shorthand, make sure value is an object
else if (value === Object(value)) {
try {
// Make sure all object keys correspond to valid config attributes
for (let name of Object.keys(value)) ServiceProviderConfig.definition.attribute(`${key}.${name}`);
// Coerce the value and assign it to the config property
Object.assign(target, ServiceProviderConfig.definition.attribute(key)
.coerce({...target, supported: true, ...value}));
} catch (ex) {
// Rethrow exceptions after giving them better context
ex.message = "SCIM configuration: " + ex.message[0].toLowerCase() + ex.message.slice(1);
throw ex;
}
}
}
}
}
// Return the new config
return Config.get();
}
}
export { Config as default };