UNPKG

@universis/janitor

Version:

Universis api plugin for handling user authorization and rate limiting

197 lines (184 loc) 7.58 kB
import {ConfigurationStrategy, Args, ApplicationService} from '@themost/common'; import path from 'path'; import url from 'url'; const HTTP_METHOD_REGEXP = /^\b(POST|PUT|PATCH|DELETE)\b$/i; class ScopeString { constructor(str) { this.value = str; } toString() { return this.value; } /** * Splits a comma-separated or space-separated scope string e.g. "profile email" or "profile,email" * * Important note: https://www.rfc-editor.org/rfc/rfc6749#section-3.3 defines the regular expression of access token scopes * which is a space separated string. Several OAuth2 servers use a comma-separated list instead. * * The operation will try to use both implementations by excluding comma ',' from access token regular expressions * @returns {Array<string>} */ split() { // the default regular expression includes comma /([\x21\x23-\x5B\x5D-\x7E]+)/g // the modified regular expression excludes comma /x2C /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g const re = /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g const results = []; let match = re.exec(this.value); while(match !== null) { results.push(match[0]); match = re.exec(this.value); } return results; } } class ScopeAccessConfiguration extends ConfigurationStrategy { /** * @param {import('@themost/common').ConfigurationBase} configuration */ constructor(configuration) { super(configuration); let elements = []; // define property Object.defineProperty(this, 'elements', { get: () => { return elements; }, enumerable: true }); } /** * @param {Request} req * @returns Promise<ScopeAccessConfigurationElement> */ verify(req) { return new Promise((resolve, reject) => { try { // validate request context Args.notNull(req.context,'Context'); // validate request context user Args.notNull(req.context.user,'User'); if (req.context.user.authenticationScope && req.context.user.authenticationScope.length>0) { // get original url let reqUrl = url.parse(req.originalUrl).pathname; // get user context scopes as array e.g, ['students', 'students:read'] let reqScopes = new ScopeString(req.context.user.authenticationScope).split(); // get user access based on HTTP method e.g. GET -> read access let reqAccess = HTTP_METHOD_REGEXP.test(req.method) ? 'write' : 'read'; // first phase: find element by resource and scope let result = this.elements.find(x => { // filter element by access level return new RegExp( "^" + x.resource, 'i').test(reqUrl) // and scopes && x.scope.find(y => { // search user scopes (validate wildcard scope) return y === "*" || reqScopes.indexOf(y)>=0; }); }); // second phase: check access level if (result == null) { return resolve(); } // if access is missing or access is not an array if (Array.isArray(result.access) === false) { // the requested access is not allowed because the access is not defined return resolve(); } // if the requested access is not in the access array if (result.access.indexOf(reqAccess) < 0) { // the requested access is not allowed because the access levels do not match return resolve(); } // otherwise, return result return resolve(result); } return resolve(); } catch(err) { return reject(err); } }); } } /** * @class */ class DefaultScopeAccessConfiguration extends ScopeAccessConfiguration { /** * @param {import('@themost/common').ConfigurationBase} configuration */ constructor(configuration) { super(configuration); let defaults = []; // load scope access from configuration resource try { /** * @type {Array<ScopeAccessConfigurationElement>} */ defaults = require(path.resolve(configuration.getConfigurationPath(), 'scope.access.json')); } catch(err) { // if an error occurred other than module not found (there are no default access policies) if (err.code !== 'MODULE_NOT_FOUND') { // throw error throw err; } // otherwise continue } this.elements.push.apply(this.elements, defaults); } } class EnableScopeAccessConfiguration extends ApplicationService { /** * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app */ constructor(app) { super(app); // register scope access configuration app.getConfiguration().useStrategy(ScopeAccessConfiguration, DefaultScopeAccessConfiguration); } } /** * @class */ class ExtendScopeAccessConfiguration extends ApplicationService { /** * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app */ constructor(app) { super(app); // Get the additional scope access extensions from the configuration const scopeAccessExtensions = app.getConfiguration().settings.universis?.janitor?.scopeAccess.imports; if (app && app.container && scopeAccessExtensions != null) { app.container.subscribe((container) => { if (container) { const scopeAccess = app.getConfiguration().getStrategy(function ScopeAccessConfiguration() { }); if (scopeAccess != null) { for (const scopeAccessExtension of scopeAccessExtensions) { try { const elements = require(path.resolve(app.getConfiguration().getConfigurationPath(), scopeAccessExtension)); if (elements) { // add extra scope access elements scopeAccess.elements.unshift(...elements); } } catch(err) { // if an error occurred other than module not found (there are no default access policies) if (err.code !== 'MODULE_NOT_FOUND') { // throw error throw err; } } } } } }); } } } export { ScopeString, ScopeAccessConfiguration, DefaultScopeAccessConfiguration, EnableScopeAccessConfiguration, ExtendScopeAccessConfiguration }