mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
* extended .gitignore for Visual Studio 2017 * creating a lib for api3 and exposing it's swagger file * adding pilot test (for /swagger.yaml) * implementing public GET /version * setting api version to 3.0.0-alpha * creating authorization skeleton + fetching some API env variables * reusing authorization library * implementing security * forcing HTTPS and removing x-powered-by from response * moving messages to constants, creating https instance fixture * testing HTTPS requiring * testing Date header * testing permission check * testing allowed operation * refactoring + storage stub * create architecture for generic operations * beginning of READ operation * tidying the code up * basic READ part * going further with READ operation * DELETE operation * handling fields parameter * refactoring to classes * going further with SEARCH operation * refactoring file structure * filtering for SEARCH operation * preparations for fallback deduplication * CREATE operation * UPDATE operation * PATCH operation * HISTORY operation * creating more precise variant of HISTORY operation * autopruning * long for timestamps in swagger * bug fix (when search fields=srvCreated) * creating skeleton for generic collection API test * specific HISTORY skeleton * distinguish between collection logical and storage name * renaming operation to LAST MODIFIED and getting it to work * fallback for LAST MODIFIED operation * tidying a bit * LAST MODIFIED documentation * bugfix + emitting data-received * adding some validations * bugfix - remove 'token' parameter from filtering * testing and debugging generic workflow * test fix for empty db * fixing security test fixture * trying to fix Travis CI testing DB problem * multiple auth callback bugfix + adding user field on authed create/update * messages for Travis CI debugging * messages for Travis CI debugging * messages for Travis CI debugging * test fix (to be prepared for future dates in db) * test fix * adding fallback created_at filling on each create/update * STATUS operation with API permissions * querying srvDate from storage + include storage version info * bugfix of missing apiConst require * getting mongo version with read-only user rights * getting mongo current date with read-only user rights * trying to diagnose travis CI timeout * refactoring storage version caching (due to some environments problems) * making VERSION work on empty database * more fixes * skipping API HTTPS test for node 8 * making code more readable using ES6 (Promises, async + await) * extending treatments collection docs by inspecting the careportal code * tidying existing API3 tests up to allow further grow * tidying the authorization code up to increase readability and performance a bit * more refactoring to ES6 and making APIv3 files structure more extendable * normalizing incoming dates to UTC and storing utcOffset * fixing srvDate to be of node.js server, not the mongo DB * preparing test fixtures for permissions testing + skeleton for CREATE operation test * intensive CREATE operation testing + minor bug fixes * correcting the deduplication test * more deduplication testing of CREATE operation * adding test skeletons for other generic operations * added variability in filtering by date, created_at, srvModified, srvCreated fields * fixing test accordingly to previous commit * adding new collection settings for centralized apps' settings storage * trying to solve travis CI testing problem - adding default collections names * another attempt to travis CI test fix * adding some tests for READ operation * adding custom error handler (overriding bodyparser's errors) * securing settings collection more and updating swagger accordingly * making HISTORY timestamp parameter more flexible + updating swagger documentation * more testing and bug fixing * sending only HTTP status with empty body, when there is no message + minor bug fixing * more refactoring and testing (especially of UPDATE operation) * PATCH testing + adding userModified field for troubleshooting purposes * basic SEARCH operation testing * more SEARCH operation testing * adding alternative 'now' query parameter to 'Date' header to make GET easier * adding 'now' to reserved query parameters for SEARCH operation * more testing * renaming field user to subject (and modifiedBy) * bugfix - fixing RFC 2822 constant for moment parsing * storageSocket: creating skeleton for new Socket.IO namespace * storageSocket: authentication by accessToken * storageSocket: authorizing to subscribe rooms * storageSocket: emitting create, update and delete events * APIv3: adding support for swagger UI at /api/v3/swagger-ui-dist * solving some problems detected by eslint * solving some problems detected by eslint * APIv3: testing and debugging Socket.IO * APIv3: testing and debugging Socket.IO * APIv3: Socket.IO documentation * APIv3: making the sample real * APIv3: starting to create a simple tutorial MD file * APIv3: small corrections * APIv3: minor corrections after dev merge * APIv3: adding CREATE and READ operations to the tutorial.md * APIv3: adding SEARCH, LAST MODIFIED, UPDATE operations to the tutorial.md * APIv3: finishing the tutorial.md * APIv3: minor bugfix (bad location after upsert) * APIv3: refactoring SEARCH complexity * APIv3: refactoring mongoCollection complexity * APIv3: refactoring complexity * APIv3: tidying up a bit * APIv3: refactoring security (start) * APIv3: refactoring lastModified * APIv3: refactoring create (start) * APIv3: refactoring create (finish) * APIv3: refactoring delete * APIv3: refactoring history * APIv3: refactoring update * APIv3: refactoring patch * APIv3: refactoring read * APIv3: refactoring search + removing deprecated authorizationBuilder * APIv3: adding best practise for identifier constructing * APIv3: refactoring and enhancing the validation (immutable fields) * APIv3: adding security.md documentation file * APIv3: refactoring - splitting index.js into multiple files * APIv3: calculating identifier on server side + deduplicating * APIv3: refactoring cosmetics * APIv3: updating the documentation * APIv3: making basic and security tests more readable using async/await * APIv3: making the rest of tests more readable using async/await * APIv3: adapting test of previous API * APIv3: adapting test of previous API
270 lines
7.8 KiB
JavaScript
270 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
var _ = require('lodash');
|
|
var jwt = require('jsonwebtoken');
|
|
var shiroTrie = require('shiro-trie');
|
|
|
|
var consts = require('./../constants');
|
|
|
|
var log_green = '\x1B[32m';
|
|
var log_red = '\x1b[31m';
|
|
var log_reset = '\x1B[0m';
|
|
var LOG_GRANTED = log_green + 'GRANTED: ' + log_reset;
|
|
var LOG_DENIED = log_red + 'DENIED: ' + log_reset;
|
|
|
|
function mkopts (opts) {
|
|
var options = opts && !_.isEmpty(opts) ? opts : { };
|
|
if (!options.redirectDeniedURL) {
|
|
options.redirectDeniedURL = null;
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function getRemoteIP (req) {
|
|
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
}
|
|
|
|
function init (env, ctx) {
|
|
var authorization = { };
|
|
var storage = authorization.storage = require('./storage')(env, ctx);
|
|
var defaultRoles = (env.settings.authDefaultRoles || '').split(/[, :]/);
|
|
|
|
function extractToken (req) {
|
|
var token;
|
|
var authorization = req.header('Authorization');
|
|
|
|
if (authorization) {
|
|
var parts = authorization.split(' ');
|
|
if (parts.length === 2 && parts[0] === 'Bearer') {
|
|
token = parts[1];
|
|
}
|
|
}
|
|
|
|
if (!token && req.auth_token) {
|
|
token = req.auth_token;
|
|
}
|
|
|
|
if (!token) {
|
|
token = authorizeAccessToken(req);
|
|
}
|
|
|
|
if (token) {
|
|
req.auth_token = token;
|
|
}
|
|
|
|
return token;
|
|
}
|
|
|
|
authorization.extractToken = extractToken;
|
|
|
|
function authorizeAccessToken (req) {
|
|
|
|
var accessToken = req.query.token;
|
|
|
|
if (!accessToken && req.body) {
|
|
if (_.isArray(req.body) && req.body.length > 0 && req.body[0].token) {
|
|
accessToken = req.body[0].token;
|
|
delete req.body[0].token;
|
|
} else if (req.body.token) {
|
|
accessToken = req.body.token;
|
|
delete req.body.token;
|
|
}
|
|
}
|
|
|
|
var authToken = null;
|
|
|
|
if (accessToken) {
|
|
// make an auth token on the fly, based on an access token
|
|
var authed = authorization.authorize(accessToken);
|
|
if (authed && authed.token) {
|
|
authToken = authed.token;
|
|
}
|
|
}
|
|
|
|
return authToken;
|
|
}
|
|
|
|
function adminSecretFromRequest (req) {
|
|
var secret = req.query && req.query.secret ? req.query.secret : req.header('api-secret');
|
|
|
|
if (!secret && req.api_secret) {
|
|
//see if we already got the secret from the body, since it gets deleted
|
|
secret = req.api_secret;
|
|
} else if (!secret && req.body) {
|
|
// try to get the secret from the body, but don't leave it there
|
|
if (_.isArray(req.body) && req.body.length > 0 && req.body[0].secret) {
|
|
secret = req.body[0].secret;
|
|
delete req.body[0].secret;
|
|
} else if (req.body.secret) {
|
|
secret = req.body.secret;
|
|
delete req.body.secret;
|
|
}
|
|
}
|
|
|
|
if (secret) {
|
|
// store the secret hash on the request since the req may get processed again
|
|
req.api_secret = secret;
|
|
}
|
|
|
|
return secret;
|
|
}
|
|
|
|
function authorizeAdminSecretWithRequest (req) {
|
|
return authorizeAdminSecret(adminSecretFromRequest(req));
|
|
}
|
|
|
|
function authorizeAdminSecret (secret) {
|
|
return (env.api_secret && env.api_secret.length > 12) ? (secret === env.api_secret) : false;
|
|
}
|
|
|
|
authorization.seenPermissions = [ ];
|
|
|
|
authorization.expandedPermissions = function expandedPermissions ( ) {
|
|
var permissions = shiroTrie.new();
|
|
permissions.add(authorization.seenPermissions);
|
|
return permissions;
|
|
};
|
|
|
|
authorization.resolveWithRequest = function resolveWithRequest (req, callback) {
|
|
authorization.resolve({
|
|
api_secret: adminSecretFromRequest(req)
|
|
, token: extractToken(req)
|
|
}, callback);
|
|
};
|
|
|
|
authorization.checkMultiple = function checkMultiple(permission, shiros) {
|
|
var found = _.find(shiros, function checkEach (shiro) {
|
|
return shiro && shiro.check(permission);
|
|
});
|
|
return _.isObject(found);
|
|
};
|
|
|
|
authorization.resolve = function resolve (data, callback) {
|
|
|
|
if (authorizeAdminSecret(data.api_secret)) {
|
|
var admin = shiroTrie.new();
|
|
admin.add(['*']);
|
|
return callback(null, { shiros: [ admin ] });
|
|
}
|
|
|
|
var defaultShiros = storage.rolesToShiros(defaultRoles);
|
|
|
|
if (data.token) {
|
|
jwt.verify(data.token, env.api_secret, function result(err, verified) {
|
|
if (err) {
|
|
return callback(err, { shiros: [ ] });
|
|
} else {
|
|
authorization.resolveAccessToken (verified.accessToken, callback, defaultShiros);
|
|
}
|
|
});
|
|
} else {
|
|
return callback(null, { shiros: defaultShiros });
|
|
}
|
|
|
|
};
|
|
|
|
authorization.resolveAccessToken = function resolveAccessToken (accessToken, callback, defaultShiros) {
|
|
|
|
if (!defaultShiros) {
|
|
defaultShiros = storage.rolesToShiros(defaultRoles);
|
|
}
|
|
|
|
let resolved = storage.resolveSubjectAndPermissions(accessToken);
|
|
if (!resolved || !resolved.subject) {
|
|
return callback('Subject not found', null);
|
|
}
|
|
|
|
let shiros = resolved.shiros.concat(defaultShiros);
|
|
return callback(null, { shiros: shiros, subject: resolved.subject });
|
|
};
|
|
|
|
authorization.isPermitted = function isPermitted (permission, opts) {
|
|
|
|
|
|
opts = mkopts(opts);
|
|
authorization.seenPermissions = _.chain(authorization.seenPermissions)
|
|
.push(permission)
|
|
.sort()
|
|
.uniq()
|
|
.value();
|
|
|
|
function check(req, res, next) {
|
|
|
|
var remoteIP = getRemoteIP(req);
|
|
|
|
if (authorizeAdminSecretWithRequest(req)) {
|
|
console.log(LOG_GRANTED, remoteIP, 'api-secret', permission);
|
|
next( );
|
|
return;
|
|
}
|
|
|
|
var token = extractToken(req);
|
|
var defaultShiros = storage.rolesToShiros(defaultRoles);
|
|
|
|
if (token) {
|
|
jwt.verify(token, env.api_secret, function result(err, verified) {
|
|
if (err) {
|
|
console.info('Error verifying Authorized Token', err);
|
|
res.status(consts.HTTP_UNAUTHORIZED).send('Unauthorized - Invalid/Missing');
|
|
} else {
|
|
var resolved = storage.resolveSubjectAndPermissions(verified.accessToken);
|
|
if (authorization.checkMultiple(permission, resolved.shiros)) {
|
|
console.log(LOG_GRANTED, remoteIP, verified.accessToken , permission);
|
|
next();
|
|
} else if (authorization.checkMultiple(permission, defaultShiros)) {
|
|
console.log(LOG_GRANTED, remoteIP, verified.accessToken, permission, 'default');
|
|
next( );
|
|
} else {
|
|
console.log(LOG_DENIED, remoteIP, verified.accessToken, permission);
|
|
res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing');
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
if (authorization.checkMultiple(permission, defaultShiros)) {
|
|
console.log(LOG_GRANTED, remoteIP, 'no-token', permission, 'default');
|
|
return next( );
|
|
}
|
|
console.log(LOG_DENIED, remoteIP, 'no-token', permission);
|
|
res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing');
|
|
}
|
|
|
|
}
|
|
|
|
return check;
|
|
};
|
|
|
|
authorization.authorize = function authorize (accessToken) {
|
|
var subject = storage.findSubject(accessToken);
|
|
|
|
var authorized = null;
|
|
|
|
if (subject) {
|
|
var token = jwt.sign( { accessToken: subject.accessToken }, env.api_secret, { expiresIn: '1h' } );
|
|
|
|
//decode so we can tell the client the issued and expired times
|
|
var decoded = jwt.decode(token);
|
|
|
|
var roles = _.uniq(subject.roles.concat(defaultRoles));
|
|
|
|
authorized = {
|
|
token: token
|
|
, sub: subject.name
|
|
// not sending roles to client to prevent us from treating them as magic
|
|
// instead group permissions by role so the we can create correct shiros on the client
|
|
, permissionGroups: _.map(roles, storage.roleToPermissions)
|
|
, iat: decoded.iat
|
|
, exp: decoded.exp
|
|
};
|
|
}
|
|
|
|
return authorized;
|
|
};
|
|
|
|
authorization.endpoints = require('./endpoints')(env, authorization);
|
|
|
|
return authorization;
|
|
}
|
|
|
|
module.exports = init;
|