openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
623 lines (517 loc) • 22.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getTransactions = getTransactions;
exports.addTransaction = addTransaction;
exports.getTransactionById = getTransactionById;
exports.findTransactionByClientId = findTransactionByClientId;
exports.updateTransaction = updateTransaction;
exports.removeTransaction = removeTransaction;
var _winston = require('winston');
var _winston2 = _interopRequireDefault(_winston);
var _statsdClient = require('statsd-client');
var _statsdClient2 = _interopRequireDefault(_statsdClient);
var _os = require('os');
var _os2 = _interopRequireDefault(_os);
var _transactions = require('../model/transactions');
var _events = require('../middleware/events');
var events = _interopRequireWildcard(_events);
var _channels = require('../model/channels');
var _autoRetry = require('../autoRetry');
var autoRetryUtils = _interopRequireWildcard(_autoRetry);
var _authorisation = require('./authorisation');
var authorisation = _interopRequireWildcard(_authorisation);
var _utils = require('../utils');
var utils = _interopRequireWildcard(_utils);
var _config = require('../config');
var _util = require('util');
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const statsdServer = _config.config.get('statsd');
const sdc = new _statsdClient2.default(statsdServer);
const application = _config.config.get('application');
const apiConf = _config.config.get('api');
const domain = `${_os2.default.hostname()}.${application.name}`;
function hasError(updates) {
if (updates.error != null) {
return true;
}
if (updates.routes != null) {
for (const route of updates.routes) {
if (route.error) {
return true;
}
}
}
if (updates.$push != null && updates.$push.routes != null && updates.$push.routes.error != null) {
return true;
}
return false;
}
function getChannelIDsArray(channels) {
const channelIDs = [];
for (const channel of Array.from(channels)) {
channelIDs.push(channel._id.toString());
}
return channelIDs;
}
// function to construct projection object
function getProjectionObject(filterRepresentation) {
switch (filterRepresentation) {
case 'simpledetails':
// view minimum required data for transaction details view
return {
'request.body': 0,
'response.body': 0,
'routes.request.body': 0,
'routes.response.body': 0,
'orchestrations.request.body': 0,
'orchestrations.response.body': 0
};
case 'full':
// view all transaction data
return {};
case 'fulltruncate':
// same as full
return {};
case 'bulkrerun':
// view only 'bulkrerun' properties
return { _id: 1, childIDs: 1, canRerun: 1, channelID: 1 };
default:
// no filterRepresentation supplied - simple view
// view minimum required data for transactions
return {
'request.body': 0,
'request.headers': 0,
'response.body': 0,
'response.headers': 0,
orchestrations: 0,
routes: 0
};
}
}
function truncateTransactionDetails(trx) {
const truncateSize = apiConf.truncateSize != null ? apiConf.truncateSize : 15000;
const truncateAppend = apiConf.truncateAppend != null ? apiConf.truncateAppend : '\n[truncated ...]';
function trunc(t) {
if ((t.request != null ? t.request.body : undefined) != null && t.request.body.length > truncateSize) {
t.request.body = t.request.body.slice(0, truncateSize) + truncateAppend;
}
if ((t.response != null ? t.response.body : undefined) != null && t.response.body.length > truncateSize) {
t.response.body = t.response.body.slice(0, truncateSize) + truncateAppend;
}
}
trunc(trx);
if (trx.routes != null) {
for (const r of Array.from(trx.routes)) {
trunc(r);
}
}
if (trx.orchestrations != null) {
return Array.from(trx.orchestrations).map(o => trunc(o));
}
}
/*
* Retrieves the list of transactions
*/
async function getTransactions(ctx) {
try {
const filtersObject = ctx.request.query;
// get limit and page values
const { filterLimit } = filtersObject;
const { filterPage } = filtersObject;
let { filterRepresentation } = filtersObject;
// remove limit/page/filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
delete filtersObject.filterLimit;
delete filtersObject.filterPage;
delete filtersObject.filterRepresentation;
// determine skip amount
const filterSkip = filterPage * filterLimit;
// get filters object
const filters = filtersObject.filters != null ? JSON.parse(filtersObject.filters) : {};
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
// if not an admin, restrict by transactions that this user can view
const channels = await authorisation.getUserViewableChannels(ctx.authenticated);
if (!filtersObject.channelID) {
filters.channelID = { $in: getChannelIDsArray(channels) };
} else if (!Array.from(getChannelIDsArray(channels)).includes(filtersObject.channelID)) {
return utils.logAndSetResponse(ctx, 403, `Forbidden: Unauthorized channel ${filtersObject.channelID}`, 'info');
}
// set 'filterRepresentation' to default if user isnt admin
filterRepresentation = '';
}
// get projection object
const projectionFiltersObject = getProjectionObject(filterRepresentation);
if (filtersObject.channelID) {
filters.channelID = filtersObject.channelID;
}
// parse date to get it into the correct format for querying
if (filters['request.timestamp']) {
filters['request.timestamp'] = JSON.parse(filters['request.timestamp']);
}
/* Transaction Filters */
// build RegExp for transaction request path filter
if (filters['request.path']) {
filters['request.path'] = new RegExp(filters['request.path'], 'i');
}
// build RegExp for transaction request querystring filter
if (filters['request.querystring']) {
filters['request.querystring'] = new RegExp(filters['request.querystring'], 'i');
}
// response status pattern match checking
if (filters['response.status'] && utils.statusCodePatternMatch(filters['response.status'])) {
filters['response.status'] = {
$gte: filters['response.status'][0] * 100,
$lt: filters['response.status'][0] * 100 + 100
};
}
// check if properties exist
if (filters.properties) {
// we need to source the property key and re-construct filter
const key = Object.keys(filters.properties)[0];
filters[`properties.${key}`] = filters.properties[key];
// if property has no value then check if property exists instead
if (filters.properties[key] === null) {
filters[`properties.${key}`] = { $exists: true };
}
// delete the old properties filter as its not needed
delete filters.properties;
}
// parse childIDs query to get it into the correct format for querying
if (filters['childIDs']) {
filters['childIDs'] = JSON.parse(filters['childIDs']);
}
/* Route Filters */
// build RegExp for route request path filter
if (filters['routes.request.path']) {
filters['routes.request.path'] = new RegExp(filters['routes.request.path'], 'i');
}
// build RegExp for transaction request querystring filter
if (filters['routes.request.querystring']) {
filters['routes.request.querystring'] = new RegExp(filters['routes.request.querystring'], 'i');
}
// route response status pattern match checking
if (filters['routes.response.status'] && utils.statusCodePatternMatch(filters['routes.response.status'])) {
filters['routes.response.status'] = {
$gte: filters['routes.response.status'][0] * 100,
$lt: filters['routes.response.status'][0] * 100 + 100
};
}
/* orchestration Filters */
// build RegExp for orchestration request path filter
if (filters['orchestrations.request.path']) {
filters['orchestrations.request.path'] = new RegExp(filters['orchestrations.request.path'], 'i');
}
// build RegExp for transaction request querystring filter
if (filters['orchestrations.request.querystring']) {
filters['orchestrations.request.querystring'] = new RegExp(filters['orchestrations.request.querystring'], 'i');
}
// orchestration response status pattern match checking
if (filters['orchestrations.response.status'] && utils.statusCodePatternMatch(filters['orchestrations.response.status'])) {
filters['orchestrations.response.status'] = {
$gte: filters['orchestrations.response.status'][0] * 100,
$lt: filters['orchestrations.response.status'][0] * 100 + 100
};
}
// execute the query
ctx.body = await _transactions.TransactionModelAPI.find(filters, projectionFiltersObject).skip(filterSkip).limit(parseInt(filterLimit, 10)).sort({ 'request.timestamp': -1 }).exec();
if (filterRepresentation === 'fulltruncate') {
Array.from(ctx.body).map(trx => truncateTransactionDetails(trx));
}
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not retrieve transactions via the API: ${e}`, 'error');
}
}
function recursivelySearchObject(ctx, obj, ws, repeat) {
if (Array.isArray(obj)) {
return obj.forEach(value => {
if (value && typeof value === 'object') {
if (ws.has(value)) {
return;
}
ws.add(value);
return repeat(ctx, value, ws);
}
});
} else if (obj && typeof obj === 'object') {
for (const k in obj) {
const value = obj[k];
if (value && typeof value === 'object') {
if (ws.has(value)) {
return;
}
ws.add(value);
repeat(ctx, value, ws);
}
}
}
}
function enforceMaxBodiesSize(ctx, obj, ws) {
if (obj.request && typeof obj.request.body === 'string') {
if (utils.enforceMaxBodiesSize(ctx, obj.request) && ctx.primaryRequest) {
obj.canRerun = false;
}
}
ctx.primaryRequest = false;
if (obj.response && typeof obj.response.body === 'string') {
utils.enforceMaxBodiesSize(ctx, obj.response);
}
return recursivelySearchObject(ctx, obj, ws, enforceMaxBodiesSize);
}
function calculateTransactionBodiesByteLength(lengthObj, obj, ws) {
if (obj.body && typeof obj.body === 'string') {
lengthObj.length += Buffer.byteLength(obj.body);
}
return recursivelySearchObject(lengthObj, obj, ws, calculateTransactionBodiesByteLength);
}
/*
* Adds an transaction
*/
async function addTransaction(ctx) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to addTransaction denied.`, 'info');
return;
}
try {
// Get the values to use
const transactionData = ctx.request.body;
const context = { primaryRequest: true };
enforceMaxBodiesSize(context, transactionData, new WeakSet());
const tx = new _transactions.TransactionModelAPI(transactionData);
// Try to add the new transaction (Call the function that emits a promise and Koa will wait for the function to complete)
await tx.save();
ctx.status = 201;
_winston2.default.info(`User ${ctx.authenticated.email} created transaction with id ${tx.id}`);
await generateEvents(tx, tx.channelID);
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not add a transaction via the API: ${e}`, 'error');
}
}
/*
* Retrieves the details for a specific transaction
*/
async function getTransactionById(ctx, transactionId) {
// Get the values to use
transactionId = unescape(transactionId);
try {
const filtersObject = ctx.request.query;
let { filterRepresentation } = filtersObject;
// remove filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
delete filtersObject.filterRepresentation;
// set filterRepresentation to 'full' if not supplied
if (!filterRepresentation) {
filterRepresentation = 'full';
}
// --------------Check if user has permission to view full content----------------- #
// if user NOT admin, determine their representation privileges.
if (!authorisation.inGroup('admin', ctx.authenticated)) {
// retrieve transaction channelID
const txChannelID = await _transactions.TransactionModelAPI.findById(transactionId, { channelID: 1 }, { _id: 0 }).exec();
if ((txChannelID != null ? txChannelID.length : undefined) === 0) {
ctx.body = `Could not find transaction with ID: ${transactionId}`;
ctx.status = 404;
return;
} else {
// assume user is not allowed to view all content - show only 'simpledetails'
filterRepresentation = 'simpledetails';
// get channel.txViewFullAcl information by channelID
const channel = await _channels.ChannelModelAPI.findById(txChannelID.channelID, { txViewFullAcl: 1 }, { _id: 0 }).exec();
// loop through user groups
for (const group of Array.from(ctx.authenticated.groups)) {
// if user role found in channel txViewFullAcl - user has access to view all content
if (channel.txViewFullAcl.indexOf(group) >= 0) {
// update filterRepresentation object to be 'full' and allow all content
filterRepresentation = 'full';
break;
}
}
}
}
// --------------Check if user has permission to view full content----------------- #
// get projection object
const projectionFiltersObject = getProjectionObject(filterRepresentation);
const result = await _transactions.TransactionModelAPI.findById(transactionId, projectionFiltersObject).exec();
if (result && filterRepresentation === 'fulltruncate') {
truncateTransactionDetails(result);
}
// Test if the result if valid
if (!result) {
ctx.body = `Could not find transaction with ID: ${transactionId}`;
ctx.status = 404;
// Test if the user is authorised
} else if (!authorisation.inGroup('admin', ctx.authenticated)) {
const channels = await authorisation.getUserViewableChannels(ctx.authenticated);
if (getChannelIDsArray(channels).indexOf(result.channelID.toString()) >= 0) {
ctx.body = result;
} else {
return utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not authenticated to retrieve transaction ${transactionId}`, 'info');
}
} else {
ctx.body = result;
}
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not get transaction by ID via the API: ${e}`, 'error');
}
}
/*
* Retrieves all transactions specified by clientId
*/
async function findTransactionByClientId(ctx, clientId) {
clientId = unescape(clientId);
try {
// get projection object
const projectionFiltersObject = getProjectionObject(ctx.request.query.filterRepresentation);
const filtersObject = { clientID: clientId
// Test if the user is authorised
};if (!authorisation.inGroup('admin', ctx.authenticated)) {
// if not an admin, restrict by transactions that this user can view
const channels = await authorisation.getUserViewableChannels(ctx.authenticated);
filtersObject.channelID = { $in: getChannelIDsArray(channels) };
}
// execute the query
ctx.body = await _transactions.TransactionModelAPI.find(filtersObject, projectionFiltersObject).sort({ 'request.timestamp': -1 }).exec();
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not get transaction by clientID via the API: ${e}`, 'error');
}
}
async function generateEvents(transaction, channelID) {
try {
_winston2.default.debug(`Storing events for transaction: ${transaction._id}`);
const channel = await _channels.ChannelModelAPI.findById(channelID);
const trxEvents = [];
events.createTransactionEvents(trxEvents, transaction, channel);
if (trxEvents.length > 0) {
await (0, _util.promisify)(events.saveEvents)(trxEvents);
}
} catch (err) {
_winston2.default.error(err);
}
}
function updateTransactionMetrics(updates, doc) {
if (updates.$push == null || updates.$push.routes === null) {
return;
}
for (const k in updates.$push) {
const route = updates.$push[k];
if (route.metrics != null) {
for (const metric of Array.from(route.metrics)) {
if (metric.type === 'counter') {
_winston2.default.debug(`incrementing mediator counter ${metric.name}`);
sdc.increment(`${domain}.channels.${doc.channelID}.${route.name}.mediator_metrics.${metric.name}`);
}
if (metric.type === 'timer') {
_winston2.default.debug(`incrementing mediator timer ${metric.name}`);
sdc.timing(`${domain}.channels.${doc.channelID}.${route.name}.mediator_metrics.${metric.name}`, metric.value);
}
if (metric.type === 'gauge') {
_winston2.default.debug(`incrementing mediator gauge ${metric.name}`);
sdc.gauge(`${domain}.channels.${doc.channelID}.${route.name}.mediator_metrics.${metric.name}`, metric.value);
}
}
}
if (route.orchestrations) {
for (const orchestration of route.orchestrations) {
const orchestrationDuration = orchestration.response.timestamp - orchestration.request.timestamp;
const orchestrationStatus = orchestration.response.status;
let orchestrationName = orchestration.name;
if (orchestration.group) {
orchestrationName = `${orchestration.group}.${orchestration.name}`; // Namespace it by group
}
/*
* Update timers
*/
_winston2.default.debug('updating async route timers');
sdc.timing(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}`, orchestrationDuration);
sdc.timing(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}.statusCodes.${orchestrationStatus}`, orchestrationDuration);
/*
* Update counters
*/
_winston2.default.debug('updating async route counters');
sdc.increment(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}`);
sdc.increment(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}.statusCodes.${orchestrationStatus}`);
if (orchestration.metrics != null) {
for (const metric of Array.from(orchestration.metrics)) {
if (metric.type === 'counter') {
_winston2.default.debug(`incrementing ${route.name} orchestration counter ${metric.name}`);
sdc.increment(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}.${metric.name}`, metric.value);
}
if (metric.type === 'timer') {
_winston2.default.debug(`incrementing ${route.name} orchestration timer ${metric.name}`);
sdc.timing(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}.${metric.name}`, metric.value);
}
if (metric.type === 'gauge') {
_winston2.default.debug(`incrementing ${route.name} orchestration gauge ${metric.name}`);
sdc.gauge(`${domain}.channels.${doc.channelID}.${route.name}.orchestrations.${orchestrationName}.${metric.name}`, metric.value);
}
}
}
}
}
}
}
/*
* Updates a transaction record specified by transactionId
*/
async function updateTransaction(ctx, transactionId) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to updateTransaction denied.`, 'info');
return;
}
transactionId = unescape(transactionId);
const updates = ctx.request.body;
try {
if (hasError(updates)) {
const transaction = await _transactions.TransactionModelAPI.findById(transactionId).exec();
const channel = await _channels.ChannelModelAPI.findById(transaction.channelID).exec();
if (!autoRetryUtils.reachedMaxAttempts(transaction, channel)) {
updates.autoRetry = true;
await autoRetryUtils.queueForRetry(transaction);
}
}
const transactionToUpdate = await _transactions.TransactionModelAPI.findOne({ _id: transactionId }).exec();
const transactionBodiesLength = { length: 0 };
calculateTransactionBodiesByteLength(transactionBodiesLength, transactionToUpdate, new WeakSet());
const context = {
totalBodyLength: transactionBodiesLength.length,
primaryRequest: true
};
enforceMaxBodiesSize(context, updates, new WeakSet());
const updatedTransaction = await _transactions.TransactionModelAPI.findByIdAndUpdate(transactionId, updates, { new: true }).exec();
ctx.body = `Transaction with ID: ${transactionId} successfully updated`;
ctx.status = 200;
_winston2.default.info(`User ${ctx.authenticated.email} updated transaction with id ${transactionId}`);
await generateEvents(updates, updatedTransaction.channelID);
updateTransactionMetrics(updates, updatedTransaction);
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not update transaction via the API: ${e}`, 'error');
}
}
/*
* Removes a transaction
*/
async function removeTransaction(ctx, transactionId) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to removeTransaction denied.`, 'info');
return;
}
// Get the values to use
transactionId = unescape(transactionId);
try {
await _transactions.TransactionModelAPI.findByIdAndRemove(transactionId).exec();
ctx.body = 'Transaction successfully deleted';
ctx.status = 200;
_winston2.default.info(`User ${ctx.authenticated.email} removed transaction with id ${transactionId}`);
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not remove transaction via the API: ${e}`, 'error');
}
}
if (process.env.NODE_ENV === 'test') {
exports.calculateTransactionBodiesByteLength = calculateTransactionBodiesByteLength;
exports.updateTransactionMetrics = updateTransactionMetrics;
}
//# sourceMappingURL=transactions.js.map