@nois/sails-util-mvcsloader
Version:
Load models, controllers, services, policies and config from specified directories and inject them into the main Sails app.
238 lines (203 loc) • 9.66 kB
JavaScript
/**
* Created by jaumard on 12/05/2015.
*/
const _ = require('lodash')
module.exports = {
bindToSails: function (cb) {
return function (err, modules) {
if (err) { return cb(err); }
_.each(modules, function (moduleDef) {
// Add a reference to the Sails app that loaded the module
moduleDef.sails = sails;
// Bind all methods to the module context
_.bindAll(moduleDef);
});
return cb(undefined, modules);
};
},
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'), (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]), (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, (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], (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;
},
bindPolicies: function (hooker, modules) {
// Add the loaded policies to our internal dictionary.
_.extend(hooker.middleware, modules);
_.extend(sails.hooks.policies.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(hooker.middleware, sails.config.policies.moduleDefinitions);
}
// Validate that all policies are functions.
try {
_.each(_.keys(hooker.middleware), (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) {
console.log(colors.red('Error occurred while loading plugin\'s policies'));
console.log(e);
// return cb(e);
}
// Set the _middlewareType property on each policy.
_.each(hooker.middleware, (policyFn, policyName) => {
policyFn._middlewareType = 'POLICY: ' + policyName;
});
// Build / normalize policy config
this.mapping = this.buildPolicyMap();
// Register action middleware for each item in the map
_.each(this.mapping, (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');
}
}