Files
cgm-remote-monitor/lib/authorization/index.js
T
Petr OndrusekandSulka Haro 2dd576a629 API V3 (#4250)
* 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
2019-10-09 22:53:55 +03:00

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;