UNPKG

@celastrina/core

Version:

Javascript Framework for simplifying Microsoft Azure Functions and supporting resources.

1,232 lines (1,231 loc) 174 kB
/* * Copyright (c) 2021, KRI, LLC. * * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * @author Robert R Murrell * @copyright Robert R Murrell * @license MIT */ "use strict"; const axios = require("axios"); const moment = require("moment"); const {v4: uuidv4} = require("uuid"); const crypto = require("crypto"); const {AuthenticationContext} = require("adal-node"); const {AccessToken} = require("@azure/identity"); /** * @typedef _ManagedResourceToken * @property {string} access_token * @property {string} expires_on * @property {string} resource * @property {string} token_type * @property {string} client_id */ /** * @typedef _CelastrinaToken * @property {string} resource * @property {string} token * @property {moment.Moment} expires */ /** * @typedef _AZLogger * @function error * @function warn * @function info * @function verbose */ /** * @typedef _TraceContext * @property {string} traceparent */ /** * @typedef _ExecutionContext * @property {string} invocationId * @property {string} functionName * @property {string} functionDirectory */ /** * @typedef _AzureFunctionContext * @property {_ExecutionContext} executionContext * @property {_TraceContext} traceContext * @property {_AZLogger} log * @property {Object} bindings */ /** * @typedef _Credential * @property {string} access_token * @property {moment.Moment} expires_on * @property {string} resource * @property {string} token_type * @property {string} client_id */ /** * @type {string} */ const CELATRINA_DEFAULT_TIMEOUT = "celastrinajs.core.service.timeout.default"; /** * @type {number} */ const DEFAULT_TIMEOUT = 5000; /** * @type {{TRACE: number, ERROR: number, VERBOSE: number, INFO: number, WARN: number, THREAT: number}} */ const LOG_LEVEL = {TRACE: 0, VERBOSE: 1, INFO: 2, WARN: 3, ERROR: 4, THREAT: 5}; function _getSchema(_target, isInstance = true) { let _schema = ((isInstance) ? _target.constructor.$object : _target.$object); if(typeof _schema === "undefined" || _schema == null) return null; else return _schema; } function _getType(_target, isInstance = true) { let _schema = _getSchema(_target, isInstance); if(_schema == null) return null; else if(_schema.hasOwnProperty("type") && typeof _schema.type === "string" && _schema.type.trim().length > 0) return _schema.type; else return null; } /** * @brief Used to type-safe-check Celastrina Objects across node packages. * @description Uses static get attribute <code>static get $object</code> method to get the type string. Use this * instead of instanceof for Celastrina types as this is a package-safe check for versions 4.x and up. * @param {(Error|Class)} _class The celastrinajs typestring. * @param {Object} _object The object instance you would like to check. * @return {boolean} True, if the target types is equalt to source type, false otherwise. */ function instanceOfCelastrinaType(_class, _object) { if(((typeof _class === "undefined" || _class === null)) || ((typeof _object !== "object") || _object == null)) return false; let _ctype = _getType(_class, false); if((typeof _ctype !== "string")) return false; let _target = _object; let _otype = null; do { _otype = _getType(_target); if(_otype === _ctype) return true; _target = _target.__proto__; } while(_target != null) return false; } /** * @description Returns the default timeout set up for celatrina.<br/> * If none is specified in the Function Configuration then option argument <code>_default_</code>, which * defaults to <code>DEFAULT_TIMEOUT</code> milliseconds. * @param {number} [_default_=DEFAULT_TIMEOUT] * @return {number} */ function getDefaultTimeout(_default_ = DEFAULT_TIMEOUT) { let _timeout = process.env[CELATRINA_DEFAULT_TIMEOUT]; if(typeof _timeout !== "string") return _default_; else return Number.parseInt(_timeout, 10); } /** * CelastrinaError * @author Robert R Murrell */ class CelastrinaError extends Error { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/CelastrinaError#", type: "celastrinajs.core.CelastrinaError"}}; /** * @param {string} message * @param {int} code * @param {boolean} [drop=false] * @param {Error} [cause=null] */ constructor(message, code = 500, drop = false, cause = null) { super(message); /**@type{string}*/this.name = this.constructor.name; /**@type{Error}*/this.cause = cause; /**@type{number}*/this.code = code; /**@type{boolean}*/this.drop = drop; } /**@return {string}*/toString() { return "[" + this.name + "][" + this.code + "][" + this.drop + "]: " + this.message; } /** * @param {string} message * @param {int} code * @param {boolean} [drop=false] * @param {Error} [cause=null] * @return {CelastrinaError} */ static newError(message, code = 500, drop = false, cause = null) { return new CelastrinaError(message, code, drop, cause); } /** * @param {*} error * @param {int} code * @param {boolean} drop * @return {CelastrinaError} */ static wrapError(error, code = 500, drop = false) { let ex = error; if(typeof ex === "undefined" || ex == null) return new CelastrinaError("Unhandled Exception.", code, drop); if(instanceOfCelastrinaType(CelastrinaError, ex)) return ex; else if(typeof ex === "string" || typeof ex === "number" || typeof ex === "boolean") return new CelastrinaError(ex, code, drop); else if(ex instanceof Error) return new CelastrinaError(ex.message, code, drop, ex); else return new CelastrinaError("Unhandled Exception.",code, drop); } } /** * CelastrinaValidationError * @author Robert R Murrell */ class CelastrinaValidationError extends CelastrinaError { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/CelastrinaValidationError#", type: "celastrinajs.core.CelastrinaValidationError"}}; /** * @param {string} message * @param {int} code * @param {boolean} [drop=false] * @param {string} [tag=""] * @param {Error} [cause=null] */ constructor(message, code = 400, drop = false, tag = "", cause = null) { super(message, code, drop, cause); /**@type{string}*/this.tag = tag; } /**@return {string}*/toString() { return "[" + this.name + "][" + this.code + "][" + this.drop + "][" + this.tag + "]: " + this.message; } /** * @param {string} message * @param {int} [code=400] * @param {boolean} [drop=false] * @param {string} [tag=""] * @param {Error} [cause=null] * @return {CelastrinaValidationError} */ static newValidationError(message, tag = "", drop = false, code = 400, cause = null) { return new CelastrinaValidationError(message, code, drop, tag, cause); } /** * @param {*} error * @param {int} [code=400] * @param {boolean} [drop=false] * @param {string} [tag=""] * @return {CelastrinaValidationError} */ static wrapValidationError(error, tag = "", drop = false, code = 400) { let ex = error; if(typeof ex === "undefined") return new CelastrinaValidationError("Unhandled Exception.", code, drop, tag); if(instanceOfCelastrinaType(CelastrinaValidationError, ex)) return ex; else if(typeof ex === "string" || typeof ex === "number" || typeof ex === "boolean") return new CelastrinaValidationError(ex, code, drop, tag); else if(ex instanceof Error) return new CelastrinaValidationError(ex.message, code, drop, tag, ex); else return new CelastrinaValidationError("Unhandled Exception.",code, drop, tag); } } /** * CelastrinaEvent * @author Robert R Murrell */ class CelastrinaEvent { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/CelastrinaEvent#", type: "celastrinajs.core.CelastrinaEvent"}}; /** * @param {Context} context * @param {*} [source=null] * @param {*} [data=null] * @param {moment.Moment} [time=moment()] * @param {boolean} [rejected=false] * @param {*} [cause=null] */ constructor(context, source = null, data = null, time = moment(), rejected = false, cause = null) { this._context = context; this._source = source; this._data = data; this._time = time; /**@type{boolean}*/this._rejected = rejected; /**@type{*}*/this._cause = cause; } /**@return{Context}*/get context() {return this._context;} /**@return{*}*/get source() {return this._source;} /**@return{*}*/get data() {return this._data;} /**@return{moment.Moment}*/get time() {return new moment(this._time);} /**@return{boolean}*/get isRejected() {return this._rejected;} /**@return{*}*/get cause() {return this._cause;} reject(cause = null) { this._rejected = true; this._cause = cause; } } /** * CelastrinaFunction * @author Robert R Murrell * @abstract */ class CelastrinaFunction { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/CelastrinaFunction#", type: "celastrinajs.core.CelastrinaFunction"}}; constructor(configuration) {} /** * @param {Configuration} config * @return {Promise<Context>} */ async createContext(config) {return new Context(config);} /** * @param {Context} context * @return {Promise<void>} */ async initialize(context) {} /** * @param {Context} context * @return {Promise<Subject>} */ async authenticate(context) {} /** * @param {Context} context * @return {Promise<void>} */ async authorize(context) {} /** * @param {Context} context * @return {Promise<void>} */ async validate(context) {} /** * @param {Context} context * @return {Promise<void>} */ async monitor(context) {} /** * @param {Context} context * @return {Promise<void>} */ async load(context) {} /** * @param {Context} context * @return {Promise<void>} */ async process(context) {} /** * @param {Context} context * @return {Promise<void>} */ async save(context) {} /** * @param {Context} context * @param {*} exception * @return {Promise<void>} */ async exception(context, exception) {} /** * @param {Context} context * @return {Promise<void>} */ async terminate(context) {} /** * @brief Method called by the Azure Function to execute the lifecycle. * @param {_AzureFunctionContext} azcontext The azcontext of the function. */ async execute(azcontext) {} } /** * ResourceAuthorization * @author Robert R Murrell * @abstract */ class ResourceAuthorization { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/ResourceAuthorization#", type: "celastrinajs.core.ResourceAuthorization"}}; /** * @param {string} id * @param {number} [skew=0] * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(id, skew = 0, timeout = DEFAULT_TIMEOUT) { this._id = id; this._tokens = {}; this._skew = skew; this._timeout = getDefaultTimeout(timeout); } /**@return{string}*/get id(){return this._id;} /**@return{number}*/get timeout() {return this._timeout;} /**@param{number}timeout*/set timeout(timeout) {this._timeout = getDefaultTimeout(timeout);} /** * @param {string} resource * @param {(null|undefined|{principalId?:string,timeout?:number})} [options={}] * @return {Promise<_CelastrinaToken>} * @abstract */ async _resolve(resource, options = {}) { throw CelastrinaError.newError("Not Implemented.", 501); } /** * @param {string} resource * @param {object} [options=null] * @return {Promise<_CelastrinaToken>} * @private */ async _refresh(resource, options = null) { let token = await this._resolve(resource, options); if(this._skew !== 0) token.expires.add(this._skew, "seconds"); this._tokens[resource] = token; return token; }; /** * @param {string} resource * @param {object} [options=null] * @return {Promise<_CelastrinaToken>} * @private */ async _getToken(resource, options = {}) { /** @type{_CelastrinaToken}*/let token = this._tokens[resource]; if(typeof token !== "object" || moment().isSameOrAfter(token.expires)) return await this._refresh(resource, options); else return token; } /** * Returns JUST the token string * @param {string} resource * @param {object} [options=null] * @return {Promise<string>} */ async getToken(resource, options = null) { /** @type{_CelastrinaToken}*/let token = await this._getToken(resource, options); return token.token; } /** * Returns the full token meta-data * @param {string} resource * @param {object} [options={}}] * @return {Promise<_CelastrinaToken>} */ async getAccessToken(resource, options = {}) { return this._getToken(resource, options); } } /** * ManagedIdentityResource * @author Robert R Murrell */ class ManagedIdentityResource extends ResourceAuthorization { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/ManagedIdentityResource#", type: "celastrinajs.core.identity.managed"}}; /**@type{string}*/static MANAGED_IDENTITY = "celastrinajs.core.identity.managed"; /** * @param {boolean}[stripDefaultRoleIdentifier=true] * @param {number}[skew=0] * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(stripDefaultRoleIdentifier = true, skew = 0, timeout = DEFAULT_TIMEOUT) { super(ManagedIdentityResource.MANAGED_IDENTITY, skew, timeout); this._strip = stripDefaultRoleIdentifier; /**@type{(null|string)}*/this._default = null; this._mappings = {}; } /**@return{boolean}*/get stripDefaultRoleIdentifier() {return this._strip;} /**@return{(null|string)}*/get defaultPrincipal() {return this._default;} /**@param{(null|string)}principalId*/set defaultPrincipal(principalId) { (typeof principalId === "string" && principalId.trim().length > 0) ? this._default = principalId.trim() : this._default = null; } /** * @param {string} principalId * @param {string} resource * @return {ManagedIdentityResource} */ addResourceMapping(principalId, resource) { if(typeof principalId !== "string" || principalId.trim().length === 0) throw CelastrinaValidationError.newValidationError("Argument 'principalId' is required.", "principalId"); if(typeof resource !== "string" || resource.trim().length === 0) throw CelastrinaValidationError.newValidationError("Argument 'resource' is required.", "resource"); this._mappings[resource.trim()] = principalId.trim(); return this; } /** * @param {{principal?:string, resource?:string}} mapping * @return {ManagedIdentityResource} */ addResourceMappingObject(mapping) { this.addResourceMapping(mapping.principal, mapping.resource); return this; } /** * @param {string} resource * @return {Promise<(null|string)>} */ async getPrincipalForResource(resource) { let _principalId = this._mappings[resource]; if(typeof _principalId !== "string") _principalId = this._default; return _principalId; } /** * @param {string} resource * @param {(null|{principalId?:string,timeout?:number})} [options=null] * @return {Promise<_CelastrinaToken>} */ async _resolve(resource, options = null) { try { let _params = new URLSearchParams(); _params.set("api-version", "2019-08-01"); /**@type{boolean}*/let _strip = this._strip; /**@type{Object}*/let _config = {timeout: this._timeout}; if(_strip) resource = resource.replace("/.default", ""); _params.set("resource", resource); let _principal = null; if(options != null) { if(typeof options.principalId === "string") _principal = options.principalId; if(typeof options.timeout === "number") _config.timeout = options.timeout; } if(typeof _principal !== "string") _principal = await this.getPrincipalForResource(resource); if(_principal != null) _params.set("principal_id", _principal); _config.params = _params; _config.headers = {"x-identity-header": process.env["IDENTITY_HEADER"]}; let response = await axios.get(process.env["IDENTITY_ENDPOINT"], _config); return { resource: resource, token: response.data.access_token, expires: moment(response.data.expires_on) }; } catch(exception) { if(typeof exception === "object" && exception.hasOwnProperty("response")) { if(exception.response.status === 404) throw CelastrinaError.newError("Resource '" + resource + "' not found.", 404); else { let status = exception.response.statusText; let msg = "Exception getting resource '" + resource + "'"; (typeof status !== "string") ? msg += "." : msg += ": " + status; throw CelastrinaError.newError(msg, exception.response.status); } } else throw CelastrinaError.newError("Exception getting resource '" + resource + "'.", 500, false, exception); } } } /** * AppRegistrationResource * @author Robert R Murrell */ class AppRegistrationResource extends ResourceAuthorization { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/AppRegistrationResource#", type: "celastrinajs.core.AppRegistrationResource"}}; /** * @param {string} id * @param {string} authority * @param {string} tenant * @param {string} secret * @param {number} [skew=0] * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(id, authority, tenant, secret, skew = 0, timeout = DEFAULT_TIMEOUT) { super(id, skew, timeout); this._authority = authority; this._tenant = tenant; this._secret = secret; } /**@return{string}*/get authority(){return this._authority;} /**@return{string}*/get tenant(){return this._tenant;} /**@return{string}*/get secret(){return this._secret;} /** * @param {string} resource * @param {(null|undefined|{principalId?:string,timeout?:number})} [options={}] * @return {Promise<_CelastrinaToken>} * @private */ async _resolve(resource, options = {}) { return new Promise((resolve, reject) => { try { let adContext = new AuthenticationContext(this._authority + "/" + this._tenant); adContext.acquireTokenWithClientCredentials(resource, this._id, this._secret, (err, response) => { if (err) reject(CelastrinaError.newError("Not authorized.", 401)); else { let token = { resource: resource, token: response.accessToken, expires: moment(response.expiresOn) }; resolve(token); } }); } catch(exception) { reject(exception); } }); } } /** * ResourceManagerTokenCredential * @author Robert R Murrell */ class ResourceManagerTokenCredential { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/ResourceManagerTokenCredential#", type: "celastrinajs.core.ResourceManagerTokenCredential"}}; /** * @param {ResourceAuthorization} ra */ constructor(ra) { /**@type{ResourceAuthorization}*/this._ra = ra; }; /**@return{ResourceAuthorization}*/get resourceAuthorization() {return this._ra;} /** * @param {(string|Array<string>)} scopes * @param {object} [options={}}] * @return {Promise<AccessToken>} */ async getToken(scopes, options = {}) { let scope = scopes; if(Array.isArray(scopes)) { if(scopes.length >= 1) scope = scopes[0]; else throw CelastrinaValidationError.newValidationError("Argument 'scopes' is required and must contain at least 1 scope.", "TokenCredential.[scopes]"); } if(typeof scope !== "string" || scope.trim().length === 0) throw CelastrinaValidationError.newValidationError("Argument 'scope' is required.", "TokenCredential.scopes"); let _at = await this._ra.getAccessToken(scope, options); return /**@type{AccessToken}*/{token: _at.token, expiresOnTimestamp: _at.expires.unix()}; } } /** * ResourceManager * @author Robert R Murrell */ class ResourceManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/ResourceManager#", type: "celastrinajs.core.ResourceManager"}}; /** * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(timeout = DEFAULT_TIMEOUT) { this._resources = {}; this._defaultTimeout = getDefaultTimeout(timeout); } /**@return{Object}*/get authorizations() {return this._resources;} /**@return{number}*/get defaultTimeout() {return this._defaultTimeout;} /**@param{number}timeout*/set defaultTimeout(timeout) {this._defaultTimeout = getDefaultTimeout(timeout);} /** * @param {ResourceAuthorization} auth * @return {Promise<ResourceManager>} */ async addResource(auth) { return this.addResourceSync(auth); } /** * @param {ResourceAuthorization} auth * @return {ResourceManager} */ addResourceSync(auth) { auth.timeout = this._defaultTimeout; this._resources[auth.id] = auth; return this; } /** * @param {string} id * @return {Promise<ResourceAuthorization>} */ async getResource(id = ManagedIdentityResource.MANAGED_IDENTITY) { return this.getResourceSync(id); } /** * @param {string} id * @return {ResourceAuthorization} */ getResourceSync(id = ManagedIdentityResource.MANAGED_IDENTITY) { let _auth = this._resources[id]; if(!instanceOfCelastrinaType(ResourceAuthorization, _auth)) return null; else return _auth; } /** * @param {string} resource * @param {string} id * @return {Promise<string>} */ async getToken(resource, id = ManagedIdentityResource.MANAGED_IDENTITY) { /**@type{ResourceAuthorization}*/let _auth = await this.getResource(id); if(_auth == null) return null; else return await _auth.getToken(resource); } /** * @param {string} id * @return {Promise<ResourceManagerTokenCredential>} */ async getTokenCredential(id = ManagedIdentityResource.MANAGED_IDENTITY) { return this.getTokenCredentialSync(id); } /** * @param {string} id * @return {ResourceManagerTokenCredential} */ getTokenCredentialSync(id = ManagedIdentityResource.MANAGED_IDENTITY) { /**@type{ResourceAuthorization}*/let _ra = this.getResourceSync(id); if(_ra == null) return null; else return new ResourceManagerTokenCredential(_ra); } /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @return {Promise<void>} */ async initialize(azcontext, config) {} /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @return {Promise<void>} */ async ready(azcontext, config) {} } /** * Vault * @author Robert R Murrell */ class Vault { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/Vault#", type: "celastrinajs.core.Vault"}}; /** * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(timeout = DEFAULT_TIMEOUT) { this._params = new URLSearchParams(); this._params.set("api-version", "7.1"); this._timeout = getDefaultTimeout(timeout); } /** * @param {string} token * @param {string} identifier * @return {Promise<string>} */ async getSecret(token, identifier) { try { let response = await axios.get(identifier, {params: this._params, headers: {"Authorization": "Bearer " + token}, timeout: this._timeout}); return response.data.value; } catch(exception) { if(typeof exception === "object" && exception.hasOwnProperty("response")) { if(exception.response.status === 404) throw CelastrinaError.newError("Vault secret '" + identifier + "' not found.", 404); else throw CelastrinaError.newError("Exception getting Vault secret '" + identifier + "': " + exception.response.statusText, exception.response.status); } else throw CelastrinaError.newError("Exception getting Vault secret '" + identifier + "'."); } } } /** * PropertyManager * @abstract * @author Robert R Murrell */ class PropertyManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/PropertyManager#", type: "celastrinajs.core.PropertyManager"}}; constructor(){} /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @return {Promise<void>} */ async initialize(azcontext, config) {} /** * @abstract * @return {string} */ get name() {return "PropertyManager";} /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @return {Promise<void>} */ async ready(azcontext, config) {} /** * @param {string} key * @return {null|*} * @abstract */ async _getProperty(key) {throw CelastrinaError.newError("Not Implemented.", 501);} /** * @param {string} key * @param {(null|*)} defaultValue * @return {Promise<{value: (null|*), defaulted: boolean}>} */ async _getPropertyOrDefault(key, defaultValue = null){ let value = await this._getProperty(key); if(typeof value === "undefined" || value == null) return {value: defaultValue, defaulted: true}; else return {value: value, defaulted: false}; } /** * @param {string} key * @param {*} [defaultValue = null] * @param {(null|StringConstructor|BooleanConstructor|NumberConstructor|ObjectConstructor|DateConstructor| * RegExpConstructor|ErrorConstructor|ArrayConstructor|ArrayBufferConstructor|DataViewConstructor| * Int8ArrayConstructor|Uint8ArrayConstructor|Uint8ClampedArrayConstructor|Int16ArrayConstructor| * Uint16ArrayConstructor|Int32ArrayConstructor|Uint32ArrayConstructor|Float32ArrayConstructor| * Float64ArrayConstructor|FunctionConstructor|function(...*))} [type = String] * @return {null|*} */ async _getConvertProperty(key, defaultValue = null, type = null) { let _response = await this._getPropertyOrDefault(key, defaultValue); if(_response.defaulted) return _response.value; else return type(_response.value); } /** * @param {string} key * @param {null|string} [defaultValue = null] * @return {Promise<string>} */ async getProperty(key, defaultValue = null) { let _response = await this._getPropertyOrDefault(key, defaultValue); return _response.value; } /** * @param {string} key * @param {*} value * @return {Promise<void>} */ async setProperty(key, value = null) {throw CelastrinaError.newError("Not Implemented.", 501);} /** * @param {string} key * @param {null|string|RegExp} [defaultValue = false] * @return {Promise<null|RegExp>} */ async getRegExp(key, defaultValue = /.*/g) { return this._getConvertProperty(key, defaultValue, RegExp); } /** * @param {string} key * @param {null|boolean} [defaultValue = false] * @return {Promise<null|boolean>} */ async getBoolean(key, defaultValue = false) { return this._getConvertProperty(key, defaultValue, Boolean); } /** * @param {string} key * @param {null|number} [defaultValue = Number.NaN] * @return {Promise<null|number>} */ async getNumber(key, defaultValue = Number.NaN) { return this._getConvertProperty(key, defaultValue, Number); } /** * @param {string} key * @param {null|Date} [defaultValue = new Date()] * @return {Promise<null|Date>} */ async getDate(key, defaultValue = new Date()) { return this._getConvertProperty(key, defaultValue, PropertyManager._createDateFromString); } /** * @param {string} key * @param {Object} [defaultValue = null] * @param {function(*)} [factory = null] * @return {Promise<Object>} */ async getObject(key, defaultValue = null, factory = null) { let _object = await this._getConvertProperty(key, defaultValue, JSON.parse); if(_object != null && factory != null) _object = factory(_object); return _object; } /** * @param {string} key * @param {function(*)} factory * @param {Object} [defaultValue = null] * @return {Promise<Object>} */ async convertObject(key, factory, defaultValue = null) { let _object = await this.getObject(key, defaultValue, factory); if(_object != null) await this.setProperty(key, _object); return _object; } /** * @param {string} key * @param {("property"|"string"|"date"|"regexp"|"number"|"boolean"|"object")} typename * @param {(null|*)} defaultValue * @param {function((null|*))} factory * @return {Promise<void>} */ async getTypedProperty(key, typename = "property", defaultValue = null, factory = null) { switch(typename) { case "property": return this.getProperty(key, defaultValue); case "string": return this.getProperty(key, defaultValue); case "date": return this.getDate(key, defaultValue); case "regexp": return this.getRegExp(key, defaultValue); case "number": return this.getNumber(key, defaultValue); case "boolean": return this.getBoolean(key, defaultValue); case "object": return this.getObject(key, defaultValue, factory); default: throw CelastrinaError.newError("Property type '" + typename + "' is invalid.", 400); } } /** * @param {string} dateTimeString * @private * @return {Date} */ static _createDateFromString(dateTimeString) { return new Date(dateTimeString); } } /** * AppSettingsPropertyManager * @author Robert R Murrell * @property {number} _timeout * @property {boolean} _followVaultReference * @property {Vault} [_authVault = null] * @property {(null|string)} [_vaultResource=null] */ class AppSettingsPropertyManager extends PropertyManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/AppSettingsPropertyManager#", type: "celastrinajs.core.AppSettingsPropertyManager"}}; /** * @param {boolean} [followVaultReference=true] * @param {string} [vaultResource=ManagedIdentityResource.MANAGED_IDENTITY] * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(followVaultReference = false, vaultResource = ManagedIdentityResource.MANAGED_IDENTITY, timeout = DEFAULT_TIMEOUT) { super(); this._timeout = getDefaultTimeout(timeout); /** @type{boolean}*/this._followVaultReference = followVaultReference; /** @type{Object}*/this._tokenOptions = null; if(this._followVaultReference) { if(typeof vaultResource !== "string" || vaultResource.trim().length === 0) throw CelastrinaValidationError.newValidationError("Arguemtn 'vaultResource' is required.", "vaultResource"); this._authVault = null; this._vault = new Vault(this._timeout); this._vaultResource = vaultResource; } else { this._authVault = null; this._vault = null; this._vaultResource = null; } } /**@return{string}*/get name() {return "AppSettingsPropertyManager";} /**@return{Vault}*/get vault() {return this._vault;} /**@return{boolean}*/get followVaultReferences() {return this._followVaultReference;} /**@param{boolean}follow*/set followVaultReferences(follow) { this._followVaultReference = follow; if(this._followVaultReference) { this._authVault = null; this._vault = new Vault(this._timeout); } else { this._authVault = null; this._vault = null; } } /**@return{number}*/get timeout() {return this._timeout;} /**@param{number}timeout*/set timeout(timeout) {this._timeout = getDefaultTimeout(timeout);} /**@return{string}*/get vaultResource() {return this._vaultResource;} /**@param{string}resource*/set vaultResource(resource) { if(typeof resource !== "string" || resource.trim().length === 0) throw new CelastrinaValidationError.newValidationError("Argument 'resource' is required.", "resource"); this._vaultResource = resource; } /** * @param {ResourceAuthorization} ra * @param {string} resource * @returns {Promise<*>} */ async getResourceTokenWithOptions(ra, resource) { return ra.getToken(resource, this._tokenOptions); } /** * @param azcontext * @param config * @return {Promise<void>} */ async initialize(azcontext, config) { let _identityOverride = process.env[Configuration.PROP_CONFIG_MI_OVERRIDE]; if(typeof _identityOverride === "string" && _identityOverride.trim().length > 0) { this._tokenOptions = {principalId: _identityOverride.trim()}; } if(this._followVaultReference) { if(this._vaultResource === ManagedIdentityResource.MANAGED_IDENTITY) { if(typeof process.env["IDENTITY_ENDPOINT"] !== "string") throw CelastrinaError.newError( "AppSettingsPropertyManager requires User or System Assigned Managed Identies to be enabled when following vault references."); } /**@type{ResourceManager}*/let _rm = config[Configuration.CONFIG_RESOURCE]; this._authVault = await _rm.getResource(this._vaultResource); if(!instanceOfCelastrinaType(ResourceAuthorization, this._authVault)) throw CelastrinaError.newError( "Vault resource authorization '" + this._vaultResource + "' not found. AppSettingsPropertyManager initialization failed."); } } /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @returns {Promise<void>} */ async ready(azcontext, config) { await super.ready(azcontext, config); this._tokenOptions = null; } /** * @param {{content_type?:string}} object * @return {boolean} */ isVaultReference(object) { if(object.hasOwnProperty("content_type") && typeof object.content_type === "string" && object.content_type.trim().length > 0) return (object.content_type.trim().toLowerCase() === "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"); else return false; } /** * @param {{content_type?:string,value?:string}} object * @return {Promise<*>} */ async resolveVaultReference(object) { try { /**@type{{uri?:string}}*/let _vlt = JSON.parse(object.value); return await this._vault.getSecret( await this.getResourceTokenWithOptions(this._authVault,"https://vault.azure.net"), _vlt.uri); } catch(exception) { throw CelastrinaError.wrapError(exception); } } /** * @param {string} key * @return {Promise<*>} * @private */ async _getProperty(key) { let _value = process.env[key]; if(this._followVaultReference) { if(typeof _value === "string" && _value.trim().length > 0) { let _obj = _value.trim(); if(_obj.startsWith("{") && _obj.endsWith("}") && _obj.indexOf("content_type") > 0) { try { _obj = JSON.parse(_obj); if(this.isVaultReference(_obj)) _value = await this.resolveVaultReference(_obj); } catch(exception) { if(instanceOfCelastrinaType(CelastrinaError, exception) && exception.code === 404) return null; else throw CelastrinaError.wrapError(exception); } } } } return _value; } } /** * AppConfigPropertyManager * @author Robert R Murrell */ class AppConfigPropertyManager extends AppSettingsPropertyManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/AppConfigPropertyManager#", type: "celastrinajs.core.AppConfigPropertyManager"}}; /** * @param {(null|string)} [configStore=null] * @param {string} [propResource=ManagedIdentityResource.MANAGED_IDENTITY] * @param {string} [vaultResource = ManagedIdentityResource.MANAGED_IDENTITY] * @param {boolean} [followVaultReference=true] * @param {number} [timeout=DEFAULT_TIMEOUT] */ constructor(configStore = null, propResource = ManagedIdentityResource.MANAGED_IDENTITY, followVaultReference = true, vaultResource = ManagedIdentityResource.MANAGED_IDENTITY, timeout = DEFAULT_TIMEOUT) { super(followVaultReference, vaultResource, timeout); /**@type{(null|string)}*/this._configStore = configStore; /**@type{(null|string)}*/this._endpoint = null; /**@type{(null|string)}*/this._propResource = propResource; /**@type{(null|string)}*/this._label = "development"; /**@type{(null|string)}*/this._version = "1.0"; /**@type {ResourceAuthorization} */this._authProp = null; /**@type{URLSearchParams}*/this._params = new URLSearchParams(); } /**@return{string}*/get name() {return "AppConfigPropertyManager";} /**@return{string}*/get configStore() {return this._configStore;} /**@param{string}store*/set configStore(store) { if(typeof store !== "string" || store.trim().length === 0) throw new CelastrinaValidationError.newValidationError("Parameter 'store' is required.", "store"); this._configStore = store; } /**@return{string}*/get propertyResource() {return this._propResource;} /**@param{string}resource*/set propertyResource(resource) { if(typeof resource !== "string" || resource.trim().length === 0) throw new CelastrinaValidationError.newValidationError("Parameter 'resource' is required.", "resource"); this._propResource = resource.trim(); } /**@return{string}*/get label() {return this._params.get("label");} /**@param{string}label*/set label(label) { if(typeof label !== "string" || label.trim().length === 0) this._label = "development"; else this._label = label; } /**@return{string}*/get apiVersion() {return this._version;} /**@param{string}version*/set apiVersion(version) { if(typeof version !== "string" || version.trim().length === 0) this._version = "1.0"; else this._version = version; } /** * @param {_AzureFunctionContext} azcontext * @param {Object} config * @return {Promise<void>} */ async initialize(azcontext, config) { await super.initialize(azcontext, config); if(typeof this._configStore !== "string" || this._configStore.trim().length === 0) throw new CelastrinaValidationError.newValidationError("Property '_configStore' is required.", "_configStore"); if(typeof this._propResource !== "string" || this._propResource.trim().length === 0) throw new CelastrinaValidationError.newValidationError("Property '_propResource' is required.", "_propResource"); this._configStore = "https://" + this._configStore.trim() + ".azconfig.io"; this._endpoint = this._configStore + "/kv/{key}"; this._params.set("label", this._label); this._params.set("api-version", this._version); if(this._authVault != null && (this._vaultResource === this._propResource)) this._authProp = this._authVault; else { if(this._propResource === ManagedIdentityResource.MANAGED_IDENTITY) { if(typeof process.env["IDENTITY_ENDPOINT"] !== "string") throw CelastrinaError.newError( "AppConfigPropertyManager requires User Assigned or System Managed Identies to be enabled."); } /**@type{ResourceManager}*/let _rm = config[Configuration.CONFIG_RESOURCE]; this._authProp = await _rm.getResource(this._propResource); if(!instanceOfCelastrinaType(ResourceAuthorization, this._authProp)) throw CelastrinaError.newError( "Property resource authorization '" + this._propResource + "' not found. AppConfigPropertyManager initialization failed."); } } /** * @param {{value?:string}} kvp * @return {Promise<string>} */ async resolveFeatureFlag(kvp) {return kvp.value;} /** * @param {{content_type?:string}} kvp * @return {boolean} */ isFeatureFlag(kvp) { if(kvp.hasOwnProperty("content_type") && typeof kvp.content_type === "string" && kvp.content_type.trim().length > 0) return (kvp.content_type.trim().toLowerCase() === "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"); else return false; } /** * @param {string} key * @return {Promise<*>} * @private */ async _getProperty(key) { try { let token = await this.getResourceTokenWithOptions(this._authProp, this._configStore); let _endpoint = this._endpoint.replace("{key}", key); let response = await axios.get(_endpoint, {params: this._params, headers: {"Authorization": "Bearer " + token, timeout: this._timeout}}); /**@type{{content_type:string,value:string}}*/let _config = response.data; if(this._followVaultReference && this.isVaultReference(_config)) return await this.resolveVaultReference(_config); else if(this.isFeatureFlag(_config)) return await this.resolveFeatureFlag(_config); else return _config.value; } catch(exception) { if(instanceOfCelastrinaType(CelastrinaError, exception)) throw exception; else if(typeof exception === "object" && exception.hasOwnProperty("response")) { if(exception.response.status === 404) return await super._getProperty(key); // Attempt to get an override locally. else throw CelastrinaError.newError("Exception getting App Configuration '" + key + "': " + exception.response.statusText, exception.response.status); } else throw CelastrinaError.newError("Exception getting App Configuration '" + key + "'."); } } } /** * CacheProperty * @author Robert R Murrell */ class CacheProperty { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/core/CacheProperty#", type: "celastrinajs.core.CacheProperty"}}; /** * @param {*} [value = null] * @param {boolean} [cache = true] * @param {number} [time=5] * @param {moment.DurationInputArg2} [unit="minutes"] */ constructor(value = null, cache = true, time = 5, unit = "minutes") { if(time < 0) time = 0; /**@type{*}*/this._value = value; /**@type{boolean}*/this._cache = cache; /**@type{(null|moment.Moment)}*/this._expires = null; /**@type{(null|moment.Moment)}*/this._lastUpdate = null; /**@type{number}*/this._time = time; /**@type{moment.DurationInputArg2} */this._unit = unit; if(this._cache) { if(this._time > 0) this._expires = moment().add(this._time, this._unit); } else this._time = 0; if(this._value != null) this._lastUpdate = moment(); } setNoCache() { this._cache = false; this._lastUpdate = null; } setNoExpire() { this._cache = true; this._lastUpdate = null; this._time = 0; } /**@return{boolean}*/get cache() {return this._cache;} /**@return{number}*/get time() {return this._time;} /**@param{number}unit*/set time(unit) { if(this._cache) { if(unit > 0) { this._time = unit; this._expires = moment().add(this._time, this._unit); } else this._time = 0; } else this._