sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
327 lines (263 loc) • 12.3 kB
JavaScript
/**
* Module dependencies
*/
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var Err = require('../../../errors');
/**
* Policies hook
* @param {SailsApp} sails
*/
module.exports = function(sails) {
/**
* Expose `policies` hook definition
*/
var policyHookDef = {
defaults: {
// Default policy mappings (allow all)
policies: { }
},
configure: function () {
this.middleware || (this.middleware = { });
},
/**
* Initialize is fired first thing when the hook is loaded
*
* @api public
*/
initialize: function(cb) {
var self = this;
// Grab policies config & policy modules and trigger callback
self.loadMiddleware(function (err) {
if (err) { return cb(err); }
sails.log.silly('Finished loading policy middleware functions. Preparing to bind policies based on config...');
try {
self.bindPolicies();
} catch (e) { return cb(e); }
return cb();
});
},
/**
* Wipe everything and (re)load middleware from policies
* (policies.js config is already loaded at this point)
*
* @api private
*/
loadMiddleware: function(cb) {
var self = this;
// Load policy modules from disk.
sails.log.silly('Loading policy modules from app...');
sails.modules.loadPolicies(function modulesLoaded (err, modules) {
if (err) { return cb(err); }
// Add the loaded policies to our internal dictionary.
_.extend(self.middleware, modules);
// If any policies were specified when loading Sails, add those on
// top of the ones loaded from disk.
if (sails.config.policies && sails.config.policies.moduleDefinitions) {
_.extend(self.middleware, sails.config.policies.moduleDefinitions);
}
// Validate that all policies are functions.
try {
_.each(_.keys(self.middleware), function(policyName) {
// If we find a bad'n, bail out.
if (!_.isFunction(sails.hooks.policies.middleware[policyName])) {
throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY' }, new Error('Failed loading invalid policy `' + policyName + '` (expected a function, but got a `' + typeof(sails.hooks.policies.middleware[policyName]) + '`)' ));
}
});
} catch (e) {
return cb(e);
}
// Set the _middlewareType property on each policy.
_.each(self.middleware, function(policyFn, policyName) {
policyFn._middlewareType = 'POLICY: '+policyName;
});
return cb();
});
},
/**
* Curry the policy chains into the appropriate controller functions
*
* @api private
*/
bindPolicies: function() {
// Build / normalize policy config
this.mapping = this.buildPolicyMap();
// Register action middleware for each item in the map
_.each(this.mapping, function(policies, targets) {
sails.registerActionMiddleware(policies, targets);
});
// Emit event to let other hooks know we're ready to go
sails.log.silly('Policy-controller bindings complete!');
sails.emit('hook:policies:bound');
},
/**
* Build normalized, hook-internal representation of policy mapping
* by performing a non-destructive parse of `sails.config.policies`
*
* @returns {Object} mapping
* @api private
*/
buildPolicyMap: function () {
// Loop through the keys looking for the old-style "controller-based" policy config,
// and if we find it then expand it out to the new style.
_.each(_.without(_.keys(sails.config.policies), 'moduleDefinitions'), function(key) {
// Is this a plain dictionary, e.g. UserController: { '*': true } ?
if (_.isPlainObject(sails.config.policies[key])) {
// Get the controller name by stripping off the (optional) trailing "Controller"
var controller = key.replace(/Controller$/,'').toLowerCase();
// For each item (i.e. action) in the dictionary, add an entry to the config.
_.each(_.keys(sails.config.policies[key]), function(action) {
// Get the policies to attach to this action.
var policies = sails.config.policies[key][action];
// Add the target/policies mapping to sails.config.policies
sails.config.policies[controller + '/' + action.toLowerCase()] = policies;
});
// Remove the deprecated config key.
delete sails.config.policies[key];
}
// Make sure all standalone action glob keys are lowercased.
else if (key !== key.toLowerCase()) {
sails.config.policies[key.toLowerCase()] = sails.config.policies[key];
delete sails.config.policies[key];
}
});
// Sort the policy keys alphabetically, ensuring that more restrictive
// keys (e.g. user/foo) come after less restrictive (e.g. user/*).
// Ignore `moduleDefinitions` since it is a special key used to allow
// programmatic setting of policy functions.
var actionsToProtect = _.without(_.keys(sails.config.policies), 'moduleDefinitions').sort();
// Declare a "never allow" function to use when a policy of `false` is encountered.
var neverAllow = function neverAllow (req, res) {
return res.forbidden();
};
neverAllow._middlewareType = 'POLICY: false (neverAllow)';
// Declare a "never allow" function to use when a policy of `false` is encountered.
var alwaysAllow = function alwaysAllow (req, res, next) {
return next();
};
alwaysAllow._middlewareType = 'POLICY: true (alwaysAllow)';
// Loop through the keys and create the map.
var mapping = _.reduce(actionsToProtect, function (memo, target, index) {
// Allow bald `true` and `false` policies by wrapping them in an array.
if (sails.config.policies[target] === true || sails.config.policies[target] === false) {
sails.config.policies[target] = [sails.config.policies[target]];
}
// Make sure policies are contained in an array.
if (!_.isArray(sails.config.policies[target])) {
sails.config.policies[target] = [sails.config.policies[target]];
}
// Get the policies the user wants to add to this set of actions.
// Note the use of _.compact to transform [undefined] into [].
var policies = _.compact(_.map(sails.config.policies[target], function(policy) {
// If the policy is `true`, make sure it's the only one for this target.
if (policy === true) {
if (sails.config.policies[target].length > 1) {
throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: if `true` is specified, it must be the only policy in the array.'));
}
// Map `true` to the "always allow" policy.
return alwaysAllow;
}
// If the policy is `false`, make sure it's the only one for this target.
if (policy === false) {
if (sails.config.policies[target].length > 1) {
throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: if `false` is specified, it must be the only policy in the array.'));
}
// Map `false` to the "never allow" policy.
return neverAllow;
}
// If the policy is a string, make sure it corresponds to one of the policies we loaded.
if (_.isString(policy)) {
if (!sails.hooks.policies.middleware[policy.toLowerCase()]) {
throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: `' + policy + '` does not correspond to any of the loaded policies.'));
}
return sails.hooks.policies.middleware[policy.toLowerCase()];
}
// If the policy is a function, return it.
if (_.isFunction(policy)) {
policy._middlewareType = 'POLICY: ' + (policy.name || 'anonymous');
return policy;
}
// Otherwise just bail.
throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: a policy must be a string, a function or `false`.'));
}));
// Start an array of targets that this set of policies will be applied to or ignored for.
var allowDenyList = [target];
// If this is the global target, loop through the rest of the targets and exclude them
// from this one. We may change this behavior / make it optional in the future,
// but for now policies are NOT cumulative.
if (target === '*') {
(function() {
for (var i = index + 1; i < actionsToProtect.length; i++) {
var nextTarget = actionsToProtect[i];
allowDenyList.push('!' + nextTarget);
}
})();
}
// If this target is a wildcard, then any other target that matches it will
// override it. We may change this behavior / make it optional in the future,
// but for now policies are NOT cumulative.
else if (target.slice(-2) === '/*') {
(function() {
// Get a version of the target without the /*
var nakedTarget = target.slice(0,-2);
// Get a version of the target without the .
var slashTarget = target.slice(0,-1);
// If we already bound a policy to the naked target, then flag that the
// current policy should _not_ be applied to it.
if (memo[nakedTarget]) {
allowDenyList.push('!' + nakedTarget);
}
// Now run through the rest of the targets in the list, and if any of them
// start with the "slashTarget", make sure this policy does _not_ apply to them.
// So if our target is `user/foo/*`, and we see `user/foo/bar` in the list,
// we will add that to the blacklist for this policy.
for (var i = index + 1; i < actionsToProtect.length; i++) {
var nextTarget = actionsToProtect[i];
if (nextTarget.indexOf(slashTarget) === 0) {
allowDenyList.push('!' + nextTarget);
}
// As soon as we find a non-matching target, we're done (because they're
// arranged in alphabetical order).
else {
break;
}
}
})();
}
// Transform the allow/deny list into a comma-delimited string that can be
// understood by `registerActionMiddleware`.
memo[allowDenyList.join(',')] = policies;
return memo;
}, {});
return mapping;
},
};
/**
* Bind `route:typeUnknown` event handler in order to support
* the `policy: '...` route option.
*
* > This allows for manually mapping policies directly on top
* > of explicit routes; e.g. in `config/routes.js`
*/
sails.on('route:typeUnknown', function bindDirectlyToRoute (event) {
// Only pay attention to delegated route events
// if `policy` is declared in event.target
if ( !event.target || !event.target.policy ) {
return;
}
var policyId = event.target.policy.toLowerCase();
// Policy doesn't exist
if (!sails.hooks.policies.middleware[policyId] ) {
var routeAddrToDisplay = event.verb ? event.verb+' '+event.path : event.path;
Err.fatal.__UnknownPolicy__ (policyId, routeAddrToDisplay, sails.config.paths.policies);
// ^^That begins terminating the process.
return;
}//-•
// Bind policy function to route.
// Make sure to merge the target and options together, so that route options like `skipRegex`
// are still applied to requests before running the policy.
var fn = sails.hooks.policies.middleware[policyId];
sails.router.bind(event.path, fn, event.verb, _.merge(event.options, event.target));
});
return policyHookDef;
};