scimmy-routers
Version:
SCIMMY Express Routers
428 lines (375 loc) • 17.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const express = require('express');
const SCIMMY = require('scimmy');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
const express__default = /*#__PURE__*/_interopDefault(express);
const SCIMMY__default = /*#__PURE__*/_interopDefault(SCIMMY);
/**
* SCIMMY Search Endpoint Router
* @since 1.0.0
*/
class Search extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing Search POST endpoint
* @param {AuthenticationContext|typeof SCIMMY.Types.Resource} [Resource] - the resource type instance for which endpoints are being registered
* @param {AuthenticationContext} [context] - method to invoke to evaluate context passed to SCIMMY handlers
*/
constructor(Resource, context = (Resource.prototype instanceof SCIMMY__default.default.Types.Resource ? undefined : Resource)) {
super({mergeParams: true});
// Respond to POST requests for /.search endpoint
this.post("/.search", async (req, res, next) => {
try {
res.status(200).send(await (new SCIMMY__default.default.Messages.SearchRequest(req.body)).apply(Resource.prototype instanceof SCIMMY__default.default.Types.Resource ? [Resource] : undefined, await context(req)));
} catch (ex) {
next(ex);
}
});
// Respond with 404 not found to all other requests for /.search endpoint
this.use("/.search", (req, res, next) => {
next(new SCIMMY__default.default.Types.Error(404, null, "Endpoint Not Found"));
});
}
}
/**
* SCIMMY Resource Type Instance Endpoints Router
*/
class Resources extends express.Router {
/**
* Construct an instance of an express router with endpoints for a given resource type instance
* @param {typeof SCIMMY.Types.Resource} Resource - the resource type instance for which endpoints are being registered
* @param {AuthenticationContext} [context] - method to invoke to evaluate context passed to SCIMMY handlers
*/
constructor(Resource, context) {
super({mergeParams: true});
// Mount /.search endpoint for resource
this.use(new Search(Resource, context));
this.get("/", async (req, res, next) => {
try {
res.send(await new Resource(req.query).read(await context(req)));
} catch (ex) {
next(ex);
}
});
this.get("/:id", async (req, res, next) => {
try {
res.send(await new Resource(req.params.id, req.query).read(await context(req)));
} catch (ex) {
next(ex);
}
});
this.post("/", async (req, res, next) => {
try {
res.status(201).send(await new Resource(req.query).write(req.body, await context(req)));
} catch (ex) {
next(ex);
}
});
this.put("/:id", async (req, res, next) => {
try {
res.send(await new Resource(req.params.id, req.query).write(req.body, await context(req)));
} catch (ex) {
next(ex);
}
});
this.patch("/:id", async (req, res, next) => {
try {
const value = await new Resource(req.params.id, req.query).patch(req.body, await context(req));
res.status(!!value ? 200 : 204).send(value);
} catch (ex) {
next(ex);
}
});
this.delete("/:id", async (req, res, next) => {
try {
res.status(204).send(await new Resource(req.params.id).dispose(await context(req)));
} catch (ex) {
next(ex);
}
});
}
}
/**
* SCIMMY Schemas Endpoints Router
*/
class Schemas extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing Schemas
*/
constructor() {
super({mergeParams: true});
this.get("/Schemas", async (req, res, next) => {
try {
res.send(await new SCIMMY__default.default.Resources.Schema(req.query).read());
} catch (ex) {
next(ex);
}
});
this.get("/Schemas/:id", async (req, res, next) => {
try {
res.send(await new SCIMMY__default.default.Resources.Schema(req.params.id, req.query).read());
} catch (ex) {
next(ex);
}
});
}
}
/**
* SCIMMY ResourceTypes Endpoints Router
*/
class ResourceTypes extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing ResourceTypes
*/
constructor() {
super({mergeParams: true});
this.get("/ResourceTypes", async (req, res, next) => {
try {
res.send(await new SCIMMY__default.default.Resources.ResourceType(req.query).read());
} catch (ex) {
next(ex);
}
});
this.get("/ResourceTypes/:id", async (req, res, next) => {
try {
res.send(await new SCIMMY__default.default.Resources.ResourceType(req.params.id, req.query).read());
} catch (ex) {
next(ex);
}
});
}
}
/**
* SCIMMY ServiceProviderConfig Endpoints Router
*/
class ServiceProviderConfig extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing ServiceProviderConfig
*/
constructor() {
super({mergeParams: true});
this.get("/ServiceProviderConfig", async (req, res, next) => {
try {
res.send(await new SCIMMY__default.default.Resources.ServiceProviderConfig(req.query).read());
} catch (ex) {
next(ex);
}
});
}
}
/**
* SCIMMY Bulk Endpoint Router
* @since 1.0.0
*/
class Bulk extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing Bulk POST endpoint
* @param {AuthenticationContext} [context] - method to invoke to evaluate context passed to SCIMMY handlers
*/
constructor(context) {
super({mergeParams: true});
// Respond to POST requests for /Bulk endpoint
this.post("/Bulk", async (req, res, next) => {
try {
const {supported, maxPayloadSize, maxOperations} = SCIMMY__default.default.Config.get()?.bulk ?? {};
if (!supported) {
next(new SCIMMY__default.default.Types.Error(501, null, "Endpoint Not Implemented"));
} else if (Number(req.header("content-length")) > maxPayloadSize) {
next(new SCIMMY__default.default.Types.Error(413, null, `The size of the bulk operation exceeds maxPayloadSize limit (${maxPayloadSize})`));
} else {
res.status(200).send(await (new SCIMMY__default.default.Messages.BulkRequest(req.body, maxOperations)).apply(undefined, await context(req)));
}
} catch (ex) {
next(ex);
}
});
}
}
/**
* SCIMMY Me Endpoint Router
* @since 1.0.0
*/
class Me extends express.Router {
/**
* Construct an instance of an express router with endpoints for accessing "Me" endpoint
* @param {AuthenticationHandler} handler - method to invoke to get ID of authenticated SCIM user
* @param {AuthenticationContext} [context] - method to invoke to evaluate context passed to SCIMMY handlers
*/
constructor(handler, context) {
super({mergeParams: true});
// Respond to GET requests for /Me endpoint
this.get("/Me", async (req, res, next) => {
try {
const id = await handler(req);
const isDeclared = SCIMMY__default.default.Resources.declared(SCIMMY__default.default.Resources.User);
// Only get the authenticated user if Users is declared and handler returns a string
const user = (isDeclared && typeof id === "string" ? await new SCIMMY__default.default.Resources.User(id).read(await context(req)) : false);
// Set the actual location of the user resource, or respond with 501 not implemented
if (user && user?.meta?.location) res.location(user.meta.location).send(user);
else next(new SCIMMY__default.default.Types.Error(501, null, "Endpoint Not Implemented"));
} catch (ex) {
next(ex);
}
});
// Respond with 501 not implemented to all other requests for /Me endpoint
this.use("/Me$", (req, res, next) => {
next(new SCIMMY__default.default.Types.Error(501, null, "Endpoint Not Implemented"));
});
}
}
// Predefined SCIM Service Provider Config authentication scheme types
const authSchemeTypes = {
oauth: {
type: "oauth2",
name: "OAuth 2.0 Authorization Framework",
description: "Authentication scheme using the OAuth 2.0 Authorization Framework Standard",
specUri: "https://datatracker.ietf.org/doc/html/rfc6749"
},
bearer: {
type: "oauthbearertoken",
name: "OAuth Bearer Token",
description: "Authentication scheme using the OAuth Bearer Token Standard",
specUri: "https://datatracker.ietf.org/doc/html/rfc6750"
},
basic: {
type: "httpbasic",
name: "HTTP Basic",
description: "Authentication scheme using the HTTP Basic Standard",
specUri: "https://datatracker.ietf.org/doc/html/rfc2617"
},
digest: {
type: "httpdigest",
name: "HTTP Digest",
description: "Authentication scheme using the HTTP Digest Standard",
specUri: "https://datatracker.ietf.org/doc/html/rfc2617"
}
};
/**
* Method invoked to authenticate a SCIM request
* @callback AuthenticationHandler
* @param {express.Request} req - the express request to be authenticated
* @returns {String|Promise<String>} the ID of the currently authenticated user, to be consumed by the /Me endpoint
* @private
*/
/**
* Method invoked to provide authentication context to a SCIM request
* @callback AuthenticationContext
* @param {express.Request} req - the express request to provide authentication context for
* @returns {*|Promise<*>} Any information to pass through to a Resource's handler methods
* @private
*/
/**
* Method invoked to determine a base URI for location properties in a SCIM response
* @callback AuthenticationBaseUri
* @param {express.Request} req - the express request to provide the base URI for
* @returns {String|Promise<String>} the base URI to use for location properties in SCIM responses
* @private
*/
/**
* SCIMMY HTTP Routers Class
*/
class SCIMMYRouters extends express.Router {
/**
* Construct a new instance of SCIMMYRouters, validate authentication scheme, and set SCIM Service Provider Configuration
* @param {Object} authScheme - details of the means of authenticating SCIM requests
* @param {String} authScheme.type - SCIM service provider authentication scheme type
* @param {AuthenticationHandler} authScheme.handler Method to invoke to authenticate SCIM requests
* @param {AuthenticationContext} [authScheme.context] Method to invoke to evaluate context passed to SCIMMY handlers
* @param {AuthenticationBaseUri} [authScheme.baseUri] Method to invoke to determine the URL to use as the base URI for any location properties in responses
* @param {String} [authScheme.docUri] URL to use as documentation URI for service provider authentication scheme
*/
constructor(authScheme = {}) {
const {type, docUri, handler, context = (() => {}), baseUri = (() => {})} = authScheme;
super({mergeParams: true});
// Make sure supplied authentication scheme is valid
if (type === undefined)
throw new TypeError("Missing required parameter 'type' from authentication scheme in SCIMMYRouters constructor");
if (handler === undefined)
throw new TypeError("Missing required parameter 'handler' from authentication scheme in SCIMMYRouters constructor");
if (typeof handler !== "function")
throw new TypeError("Parameter 'handler' must be of type 'function' for authentication scheme in SCIMMYRouters constructor");
if (authSchemeTypes[type] === undefined)
throw new TypeError(`Unknown authentication scheme type '${type}' in SCIMMYRouters constructor`);
if (typeof context !== "function")
throw new TypeError("Parameter 'context' must be of type 'function' for authentication scheme in SCIMMYRouters constructor");
if (typeof baseUri !== "function")
throw new TypeError("Parameter 'baseUri' must be of type 'function' for authentication scheme in SCIMMYRouters constructor");
// Register the authentication scheme, and other SCIM Service Provider Config options
SCIMMY__default.default.Config.set({
patch: true, filter: true, sort: true, bulk: true,
authenticationSchemes: [{...authSchemeTypes[type], documentationUri: docUri}]
});
// Make sure SCIM JSON is decoded in request body
this.use(express__default.default.json({type: ["application/scim+json", "application/json"], limit: SCIMMY__default.default.Config.get()?.bulk?.maxPayloadSize ?? "1mb"}));
// Listen for incoming requests to determine basepath for all resource types
this.use("/", async (req, res, next) => {
// Set correct header for SCIM responses
res.setHeader("Content-Type", "application/scim+json");
try {
// Evaluate the request-based basepath location
const basepath = await baseUri(req) ?? "";
// Make sure it's a valid URL string
if (!basepath || typeof basepath === "string" && basepath.match(/^https?:\/\//)) {
// Construct the actual basepath to use for resource locations...
const location = basepath.replace(/\/$/, "") + req.baseUrl;
// ...then set all resource basepaths correctly
SCIMMY__default.default.Resources.Schema.basepath(location);
SCIMMY__default.default.Resources.ResourceType.basepath(location);
SCIMMY__default.default.Resources.ServiceProviderConfig.basepath(location);
for (let Resource of Object.values(SCIMMY__default.default.Resources.declared()))
Resource.basepath(location);
next();
} else {
next(new TypeError("Method 'baseUri' must return a URL string in SCIMMYRouters constructor"));
}
} catch (ex) {
next(ex);
}
});
// Make sure requests are authenticated using supplied auth handler method
this.use(async (req, res, next) => {
try {
// Run the handler
await handler(req);
next();
} catch (ex) {
// Wrap exceptions in unauthorized message
res.status(401).send(new SCIMMY__default.default.Messages.Error({status: 401, message: ex.message}));
}
});
// Cast pagination query parameters from strings to numbers...
this.use(({query}, res, next) => {
for (let param of ["startIndex", "count"]) {
// ...but only if they were defined, were strings, and are valid as numbers
if (!!query[param] && typeof query[param] === "string" && !Number.isNaN(+query[param])) {
query[param] = +query[param];
}
}
next();
});
// Register core service provider endpoints
this.use(new Schemas());
this.use(new ResourceTypes());
this.use(new ServiceProviderConfig());
this.use(new Search(context));
this.use(new Bulk(context));
this.use(new Me(handler, context));
// Register endpoints for any declared resource types
for (let Resource of Object.values(SCIMMY__default.default.Resources.declared())) {
this.use(Resource.endpoint, new Resources(Resource, context));
}
// If we get to this point, there's no matching endpoints
this.use((req, res) => res.status(404).send(new SCIMMY__default.default.Messages.Error({status: 404, message: "Endpoint Not Found"})));
// Handle any middleware exceptions, and if necessary, forward to next middleware
this.use((ex, req, res, next) => {
res.status(ex.status ?? 500).send(new SCIMMY__default.default.Messages.Error(ex));
if ((ex.status ?? 500) >= 500) next(ex);
});
}
}
Object.defineProperty(exports, "SCIMMY", {
enumerable: true,
get: function () { return SCIMMY__default.default; }
});
exports.SCIMMYRouters = SCIMMYRouters;
exports.default = SCIMMYRouters;