choreo
Version:
A Robotics Framework
294 lines (248 loc) • 9.29 kB
JavaScript
var _ = require('lodash');
var util = require('choreo-util');
var Err = require('../../../errors');
module.exports = function (choreo) {
/**
* Expose `policies` hook definition
*/
var policyHookDef = {
defaults: {
// Default policy mappings (allow all)
policies: {
'*': true
}
},
// Don't allow choreo to graph until ready
// is explicitly set below
ready: false,
configure: function () {
this.middleware || (this.middleware = {});
},
/**
* Initialize is fired first thing when the hook is loaded
*
* @api public
*/
initialize: function (cb) {
// Callback is optional
cb = util.optional(cb);
// Grab policies config & policy modules and trigger callback
this.loadMiddleware(function (err) {
if (err) return cb(err);
choreo.log.verbose(
'Finished loading policy middleware logic.');
cb();
}.bind(this));
// Before routing, curry controller functions with appropriate policy chains
choreo.on('router:before', this.bindPolicies);
},
/**
* Wipe everything and (re)load middleware from policies
* (policies.js config is already loaded at this point)
*
* @api private
*/
loadMiddleware: function (cb) {
// Load policy modules
choreo.log.verbose('Loading policy modules from app...');
choreo.modules.loadPolicies(function modulesLoaded(err, modules) {
if (err) return cb(err);
_.extend(this.middleware, modules);
//console.log('choreo.middleware.policies', choreo.middleware.policies);
return cb();
}.bind(this));
},
/**
* Curry the policy chains into the appropriate controller functions
*
* @api private
*/
bindPolicies: function () {
// Build / normalize policy config
this.mapping = this.buildPolicyMap();
_bindPolicies(this.mapping, choreo.middleware.controllers);
// Emit event to let other hooks know we're ready to go
choreo.log.verbose('Policy-controller bindings complete!');
choreo.emit('hook:policies:bound');
},
/**
* Build normalized, hook-internal representation of policy mapping
* by performing a non-destructive parse of `choreo.config.policies`
*
* @returns {Object} mapping
* @api private
*/
buildPolicyMap: function () {
var mapping = {};
_.each(choreo.config.policies, function (_policy, controllerId) {
// Accept `FooController` or `foo`
// Case-insensitive
controllerId = util.normalizeControllerId(controllerId);
// Controller-level policy ::
// Just map the policy to the controller directly
if (!util.isDictionary(_policy)) {
mapping[controllerId] = policyHookDef.normalizePolicy(
_policy);
return;
}
// Policy mapping contains a sub-object ::
// So we need to dive in and build/normalize the policy mapping from here
// Mapping each policy to each action for this controller
mapping[controllerId] = {};
_.each(_policy, function (__policy, actionId) {
// Case-insensitive
actionId = actionId.toDropCase();
mapping[controllerId][actionId] = policyHookDef.normalizePolicy(
__policy);
});
});
return mapping;
},
/**
* Convert a single policy into shallow array notation
* (look up string policies using middleware in this hook)
*
* @param {Array|String|Function|Boolean} policy
* @api private
*/
normalizePolicy: function (policy) {
// Recursively normalize lists of policies
if (_.isArray(policy)) {
// Normalize each policy in the chain
return _.flatten(
_.map(policy, function normalize_each_policy(policy) {
return policyHookDef.normalizePolicy(policy);
}));
}
// Look up the policy in the policy registry
if (_.isString(policy)) {
var policyFn = this.lookupFn(policy, 'config.policies');
// Set the "policy" key on the policy function to the policy name, for debugging
policyFn._middlewareType = 'POLICY: ' + policy;
return [policyFn];
}
// An explicitly defined, anonymous policy middleware can be directly attached
if (_.isFunction(policy)) {
var anonymousPolicy = policy.bind({});
// Set the "policy" key on the function name (if any) for debugging
anonymousPolicy._middlewareType = 'POLICY: ' + (anonymousPolicy.name ||
'anonymous');
return [anonymousPolicy];
}
// A false or null policy means NEVER allow any requests
if (policy === false || policy === null) {
var neverAllow = function neverAllow(req, res, next) {
res.send(403);
};
neverAllow._middlewareType = 'POLICY: neverAllow';
return [neverAllow];
}
// A true policy means ALWAYS allow requests
if (policy === true) {
var alwaysAllow = function alwaysAllow(req, res, next) {
next();
};
alwaysAllow._middlewareType = 'POLICY: alwaysAllow';
return [alwaysAllow];
}
// If we made it here, the policy is invalid
choreo.log.error('Cannot map invalid policy: ', policy);
return [function (req, res) {
throw new Error('Invalid policy: ' + policy);
}];
},
/**
* Bind a route directly to a policy
*/
bindDirectlyToRoute: function (event) {
// Only pay attention to delegated route events
// if `policy` is declared in event.target
if (!event.target || !event.target.policy) {
return;
}
// Bind policy function to route
var fn = this.lookupFn(event.target.policy, 'config.routes');
choreo.router.bind(event.path, fn, event.verb, _.merge(event.options,
event.target));
},
/**
* @param {String} policyId
* @param {String} referencedIn [optional]
* - where the policy identity is being referenced, for providing better error msg
* @returns {Function} the appropriate policy middleware
*/
lookupFn: function (policyId, referencedIn) {
policyId = policyId.toDropCase();
// Policy doesn't exist
if (!this.middleware[policyId]) {
return Err.fatal.__UnknownPolicy__(policyId, referencedIn,
choreo.config.paths.policies);
}
// Policy found
return this.middleware[policyId];
}
};
/**
* Bind routes for manually-mapped policies from `config/routes.js`
*/
choreo.on('route:typeUnknown', function (ev) {
policyHookDef.bindDirectlyToRoute(ev);
});
return policyHookDef;
};
/**
* Bind a set of policies to a set of controllers
* (prepend policy chains to original middleware)
*/
function _bindPolicies(mapping, middlewareSet) {
_.each(middlewareSet, function (_c, id) {
var topLevelPolicyId = mapping[id];
var actions, actionFn;
var controller = middlewareSet[id];
// If a policy doesn't exist for this controller, use '*'
if (_.isUndefined(topLevelPolicyId)) {
topLevelPolicyId = mapping['*'];
}
// Build list of actions
if (util.isDictionary(controller)) {
actions = _.functions(controller);
}
// If this is a controller policy, apply it immediately
if (!util.isDictionary(topLevelPolicyId)) {
// :: Controller is a container object
// -> apply the policy to all the actions
if (util.isDictionary(controller)) {
// choreo.log.verbose('Applying policy (' + topLevelPolicyId + ') to controller\'s (' + id + ') actions...');
_.each(actions, function (actionId) {
actionFn = controller[actionId];
controller[actionId] = topLevelPolicyId.concat([actionFn]);
// choreo.log.verbose('Applying policy to ' + id + '.' + actionId + '...', controller[actionId]);
});
return;
}
// :: Controller is a function
// -> apply the policy directly
// choreo.log.verbose('Applying policy (' + topLevelPolicyId + ') to top-level controller middleware fn (' + id + ')...');
middlewareSet[id] = topLevelPolicyId.concat(controller);
}
// If this is NOT a top-level policy, and merely a container of other policies,
// iterate through each of this controller's actions and apply policies in a way that makes sense
else {
_.each(actions, function (actionId) {
var actionPolicy = mapping[id][actionId];
// choreo.log.verbose('Mapping policies to actions.... ', actions);
// If a policy doesn't exist for this controller/action, use the controller-local '*'
if (_.isUndefined(actionPolicy)) {
actionPolicy = mapping[id]['*'];
}
// if THAT doesn't exist, use the global '*' policy
if (_.isUndefined(actionPolicy)) {
actionPolicy = mapping['*'];
}
// choreo.log.verbose('Applying policy (' + actionPolicy + ') to action (' + id + '.' + actionId + ')...');
actionFn = controller[actionId];
controller[actionId] = actionPolicy.concat([actionFn]);
});
}
});
}