UNPKG

openhim-core

Version:

The OpenHIM core application that provides logging and routing of http requests

597 lines (466 loc) 20.4 kB
"use strict"; 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 = _interopRequireDefault(require("winston")); var _transactions = require("../model/transactions"); var events = _interopRequireWildcard(require("../middleware/events")); var _channels = require("../model/channels"); var autoRetryUtils = _interopRequireWildcard(require("../autoRetry")); var authorisation = _interopRequireWildcard(require("./authorisation")); var utils = _interopRequireWildcard(require("../utils")); var _config = require("../config"); var _util = require("util"); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const apiConf = _config.config.get('api'); 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)); } } /* * Returns intersection of user and channel roles/permission groups */ function getActiveRoles(acl, userGroups, channels) { const userRoles = new Set(userGroups); const channelRoles = new Set(); channels.forEach(item => item[acl].forEach(role => channelRoles.add(role))); return new Set([...userRoles].filter(i => channelRoles.has(i))); } /* * 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 fullViewChannels = await authorisation.getUserViewableChannels(ctx.authenticated, 'txViewFullAcl'); const partViewChannels = await authorisation.getUserViewableChannels(ctx.authenticated); const allChannels = fullViewChannels.concat(partViewChannels); if (filters.channelID) { if (!getChannelIDsArray(allChannels).includes(filters.channelID)) { return utils.logAndSetResponse(ctx, 403, `Forbidden: Unauthorized channel ${filters.channelID}`, 'info'); } } else { filters.channelID = { $in: getChannelIDsArray(allChannels) }; } if (getActiveRoles('txViewFullAcl', ctx.authenticated.groups, allChannels).size > 0) { filterRepresentation = 'full'; } else if (getActiveRoles('txViewAcl', ctx.authenticated.groups, allChannels).size > 0) { filterRepresentation = 'simpledetails'; } else { filterRepresentation = ''; } } // get projection object const projectionFiltersObject = getProjectionObject(filterRepresentation); // 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; _winston.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 { _winston.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) { _winston.default.error(err); } } /* * 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; _winston.default.info(`User ${ctx.authenticated.email} updated transaction with id ${transactionId}`); await generateEvents(updates, updatedTransaction.channelID); } 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; _winston.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; } //# sourceMappingURL=transactions.js.map