authorify
Version:
Authorization and authentication system for REST server
468 lines (457 loc) • 16.4 kB
JavaScript
/**
* The authorization middleware.
*
* @class node_modules.authorify.authorization
*
* @author Marcello Gesmundo
*
* ## Example
*
* // dependencies
* var restify = require('restify');
* authorify = require('authorify')(){
* // add your options
* },
* sec = authorify.authorization;
*
* // create the server
* server = restify.createServer();
*
* // add all middlewares
* server.use(restify.queryParser({ mapParams: false }));
* server.use(restify.bodyParser());
* server.use(authorify.authentication);
*
* // define default handler
* server.get('/secure/roletest', sec.isSelfOrInRole(['user', 'guest']), function(err, res) {
* console.log('ok');
* });
*
* // add your routes
* // ...
*
* // start the server
* server.listen(3000);
*
*
* # License
*
* Copyright (c) 2012-2014 Yoovant by Marcello Gesmundo. All rights reserved.
*
* This program is released under a GNU Affero General Public License version 3 or above, which in summary means:
*
* - You __can use__ this program for __no cost__.
* - You __can use__ this program for __both personal and commercial reasons__.
* - You __do not have to share your own program's code__ which uses this program.
* - You __have to share modifications__ (e.g bug-fixes) you've made to this program.
*
* For more convoluted language, see the LICENSE file.
*
*/
module.exports = function(app) {
'use strict';
var _ = app._,
errors = app.errors,
config = app.config,
parser = app.mathParser,
debug = app.config.debug,
log = app.logger;
var _isLoggedIn = function(req) {
return (req.session && req.session.userId);
};
var _isSelf = function(req) {
var result = false;
if (_isLoggedIn(req) && req.session && req.session.userId) {
var id = req.session.userId.toString(),
field = config.userIdFieldName;
if (_.has(req.params || {}, field)) {
result = (id === req.params[field]);
} else if (_.has(req.query || {}, field)) {
result = (id === req.query[field]);
} else if (_.has(req.body || {}, field)) {
result = (id === req.body[field]);
}
}
return result;
};
var _isInRole = function(req, roles) {
var result = false;
if (_isLoggedIn(req)) {
var userRoles = req.session.roles || [];
roles = roles ? [].concat(roles) : [];
result = ((_.intersection(userRoles, roles)).length > 0);
}
return result;
};
var checkConditions = function(req, conditions) {
var result = 'ok',
repeat = true;
var whatchdog = setTimeout(function() {
return 'evaluation conditions timeout error';
}, 60000);
if (conditions) {
parser.clear();
do {
try {
result = (parser.eval(conditions) ? 'ok' : 'ko');
repeat = false;
} catch (e) {
var error = 'undefined symbol',
message = e.message.toLowerCase().trim();
if (message.startsWith(error)) {
var symbol = message.slice(error.length, message.length).trim();
if (symbol) {
if (_.has(req.params || {}, symbol)) {
parser.set(symbol, req.params[symbol]);
} else if (_.has(req.query || {}, symbol)) {
parser.set(symbol, req.query[symbol]);
} else if (_.has(req.body || {}, symbol)) {
parser.set(symbol, req.body[symbol]);
} else {
result = symbol + ' parameter not found';
repeat = false;
}
}
} else {
result = message;
repeat = false;
}
}
} while (repeat);
}
clearTimeout(whatchdog);
if (conditions) {
if (result === 'ok') {
log.debug('%s successful conditions %s', app.name, conditions);
} else if (result === 'ko') {
log.debug('%s failed conditions %s', app.name, conditions);
} else {
log.error('%s error evaluating conditions %s', app.name, conditions);
}
}
return result;
};
var setOptions = function(opts) {
opts = opts || {};
opts.nextOnError = opts.nextOnError || false;
opts.forbiddenOnFail = opts.forbiddenOnFail || false;
return opts;
};
var logSuccess = function() {
log.debug('%s successful authorized test', app.name);
};
app.authorization = {
/**
* Check if the user is logged in.
*
*
* ## Example
*
* Create a server to use in every following example.
*
* // dependencies
* var fs = require('fs'),
* path = require('path'),
* restify = require('restify'),
* authorify = require('authorify')({
* // add your config options
* });
* // create the server
* server = restify.createServer();
* // add middlewares
* server.use(restify.queryParser({ mapParams: false }));
* server.use(restify.bodyParser());
* server.use(authorify.authentication);
* // define handlers
* var ok = function(req, res, next){
* // define your response
* res.send({ success: true, message: 'ok' });
* };
* var sec = authorify.authorization;
*
*
* ## Example 1
*
*
* server.get('/secure/loggedtest',
* sec.isLoggedIn('param == 1'),
* next);
*
* request|param == 1 |logged|response
* -------|-----------|------|--------
* GET |true |true |next()
* GET |true |false |401
* GET |false |true |next()
* GET |false |false |next()
* GET |missing opt|true |403
* GET |missing opt|false |403
*
* ## Example 2
*
* server.get('/secure/loggedtest',
* sec.isLoggedIn('param == 1', { forbiddenOnFail: true }),
* next);
*
* request|param == 1 |logged|response
* -------|-----------|------|--------
* GET |true |true |next()
* GET |true |false |401
* GET |false |true |403
* GET |false |false |403
* GET |missing opt|true |403
* GET |missing opt|false |403
*
* ## Example 3
*
* server.get('/secure/loggedtest',
* sec.isLoggedIn('opt1 == 1', { nextOnError: true }),
* next);
*
* request|param == 1 |logged|response
* -------|-----------|------|--------
* GET |true |true |next()
* GET |true |false |401
* GET |false |true |next()
* GET |false |false |next()
* GET |missing opt|true |next(err)
* GET |missing opt|false |next(err)
*
* ## Example 4
*
* server.get('/secure/loggedtest',
* sec.isLoggedIn('opt1 == 1', { forbiddenOnFail: true, nextOnError: true }),
* next);
*
* request|param == 1 |logged|response
* -------|-----------|------|--------
* GET |true |true |next()
* GET |true |false |401
* GET |false |true |403
* GET |false |false |403
* GET |missing opt|true |next(err)
* GET |missing opt|false |next(err)
*
*
* @param {String} [conditions] A string with a test that will be executed before to check next condition
* @param {Object} [opts] Options to customize the behavior of the test
* @param {Boolean} [opts.nextOnError=false] When true and the test trows an error, it execute next()
* @param {Boolean} [opts.forbiddenOnFail=false] When true and the test fails (without error), it sends a 403 error
* @return {String} The result of the logged test.
*
* Values:
*
* - 'ok': if the authorization test are successful evaluated
* - 'ko': if the authorization test fails
* - 'specific error': a string with a detail about the error occurred in conditions evaluation (e.g.: missing param)
*
*/
isLoggedIn: function(conditions, opts) {
return function(req, res, next) {
opts = setOptions(opts);
var err;
var checkResult = checkConditions(req, conditions);
switch (checkResult) {
case 'ok':
if (_isLoggedIn(req)) {
logSuccess();
next();
} else {
err = new errors.UnauthorizedError('not logged in').log();
res.send(err.statusCode, err.body);
}
break;
case 'ko':
if (opts.forbiddenOnFail) {
err = new errors.ForbiddenError('failed conditions').log();
res.send(err.statusCode, err.body);
} else {
logSuccess();
next();
}
break;
default:
err = new errors.ForbiddenError(checkResult).log();
if (opts.nextOnError) {
next(err);
} else {
res.send(err.statusCode, err.body);
}
break;
}
};
},
/**
* Check if the user id specified as param is the same of the logged user. See more example about conditions
* and options in {@link #isLoggedIn} handler.
*
* ## Example
*
* server.get('/secure/user/:user', sec.isSelf(), ok);
*
* @param {String} [conditions] A string with a test that will be executed before to check next condition
* @param {Object} [opts] Options to customize the behavior of the test
* @param {Boolean} [opts.nextOnError=false] When true and the test trows an error, it execute next()
* @param {Boolean} [opts.forbiddenOnFail=false] When true and the test fails (without error), it sends a 403 error
* @return {String} The result of the logged test.
*
* Values:
*
* - 'ok': if the authorization test are successful evaluated
* - 'ko': if the authorization test fails
* - 'specific error': a string with a detail about the error occurred in conditions evaluation (e.g.: missing param)
*/
isSelf: function(conditions, opts) {
return function(req, res, next) {
opts = setOptions(opts);
var err;
var checkResult = checkConditions(req, conditions);
switch (checkResult) {
case 'ok':
if (_isSelf(req)) {
logSuccess();
next();
} else {
err = new errors.ForbiddenError('user not allowed').log();
res.send(err.statusCode, err.body);
}
break;
case 'ko':
if (opts.forbiddenOnFail) {
err = new errors.ForbiddenError('failed conditions').log();
res.send(err.statusCode, err.body);
} else {
logSuccess();
next();
}
break;
default:
err = new errors.ForbiddenError(checkResult).log();
if (opts.nextOnError) {
next(err);
} else {
res.send(err.statusCode, err.body);
}
break;
}
};
},
/**
* Check if the current user belongs at least one of the role/roles specified. See more example about conditions
* and options in {@link #isLoggedIn} handler.
*
* ## Example
*
* server.get('/secure/roletest1', sec.isInRole('admin'), ok);
* server.get('/secure/roletest2', sec.isInRole(['user', 'guest']), ok);
*
* @param {String/Array} roles A string or an array with one or more roles (at least one) to which
* the current user should belong to
* @param {String} [conditions] A string with a test that will be executed before to check next condition
* @param {Object} [opts] Options to customize the behavior of the test
* @param {Boolean} [opts.nextOnError=false] When true and the test trows an error, it execute next()
* @param {Boolean} [opts.forbiddenOnFail=false] When true and the test fails (without error), it sends a 403 error
* @return {String} The result of the logged test.
*
* Values:
*
* - 'ok': if the authorization test are successful evaluated
* - 'ko': if the authorization test fails
* - 'specific error': a string with a detail about the error occurred in conditions evaluation (e.g.: missing param)
*/
isInRole: function(roles, conditions, opts) {
return function(req, res, next) {
opts = setOptions(opts);
var err;
var checkResult = checkConditions(req, conditions);
switch (checkResult) {
case 'ok':
if (_isInRole(req, roles)) {
logSuccess();
next();
} else {
err = new errors.ForbiddenError('role not allowed').log();
if (opts.nextOnError) {
next(err);
} else {
res.send(err.statusCode, err.body);
}
}
break;
case 'ko':
if (opts.forbiddenOnFail) {
err = new errors.ForbiddenError('failed conditions').log();
res.send(err.statusCode, err.body);
} else {
logSuccess();
next();
}
break;
default:
err = new errors.ForbiddenError(checkResult).log();
res.send(err.statusCode, err.body);
break;
}
};
},
/**
* Check if the user id specified as param is the same of the logged user or the user belongs
* at least one of the role/roles specified. See more example about conditions and options in
* {@link #isLoggedIn} handler.
*
* ## Example
*
* server.get('/secure/selfrole/:user/somepath', sec.isSelfOrInRole(['admin', 'user']), ok);
*
*
* @param {String/Array} roles A string or an array with one or more roles (at least one) to which
* the current user should belong to
* @param {String} [conditions] A string with a test that will be executed before to check next condition
* @param {Object} [opts] Options to customize the behavior of the test
* @param {Boolean} [opts.nextOnError=false] When true and the test trows an error, it execute next()
* @param {Boolean} [opts.forbiddenOnFail=false] When true and the test fails (without error), it sends a 403 error
* @return {String} The result of the logged test.
*
* Values:
*
* - 'ok': if the authorization test are successful evaluated
* - 'ko': if the authorization test fails
* - 'specific error': a string with a detail about the error occurred in conditions evaluation (e.g.: missing param)
*/
isSelfOrInRole: function(roles, conditions, opts) {
return function (req, res, next) {
opts = setOptions(opts);
var err;
var checkResult = checkConditions(req, conditions);
switch (checkResult) {
case 'ok':
if (_isSelf(req) || _isInRole(req, roles)) {
logSuccess();
next();
} else {
err = new errors.ForbiddenError('user or role not allowed').log();
res.send(err.statusCode, err.body);
}
break;
case 'ko':
if (opts.forbiddenOnFail) {
err = new errors.ForbiddenError('failed conditions').log();
res.send(err.statusCode, err.body);
} else {
logSuccess();
next();
}
break;
default:
err = new errors.ForbiddenError(checkResult.log());
if (opts.nextOnError) {
next(err);
} else {
res.send(err.statusCode, err.body);
}
break;
}
};
}
};
return app;
};