@celastrina/core
Version:
Javascript Framework for simplifying Microsoft Azure Functions and supporting resources.
1,232 lines (1,231 loc) • 174 kB
JavaScript
/*
* 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._