Skip to content

Commit

Permalink
Update policies hook and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sgress454 committed Oct 18, 2016
1 parent d297225 commit 66d2b2d
Show file tree
Hide file tree
Showing 4 changed files with 665 additions and 492 deletions.
10 changes: 7 additions & 3 deletions lib/app/register-action-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ module.exports = function registerAction(middleware, actions) {

// TODO -- update machine-as-action with a response type that calls `next`,
// so machine defs can be registered as middleware?
if (!_.isFunction(middleware)) {
throw new Error('Attempted to register middleware for `' + actions + '` but the provided middleware was not a function (it was a ' + typeof(middleware) + ').');
if (!_.isArray(middleware)) {
middleware = [middleware];
}

if (!_.all(middleware, _.isFunction)) {
throw new Error('Attempted to register middleware for `' + actions + '` but one or more of the provided middleware was not a function.');
}

// Get or create the array for this middleware key.
var middlewareForKey = this._actionMiddleware[actions] || [];

// Add this middleware to the array.
middlewareForKey.push(middleware);
middlewareForKey = middlewareForKey.concat(middleware);

// Assign the array back to the dictionary.
this._actionMiddleware[actions] = middlewareForKey;
Expand Down
297 changes: 135 additions & 162 deletions lib/hooks/policies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = function(sails) {
defaults: {

// Default policy mappings (allow all)
policies: { '*': true }
policies: { }
},

configure: function () {
Expand All @@ -25,6 +25,7 @@ module.exports = function(sails) {
* @api public
*/
initialize: function(cb) {

// Callback is optional
cb = util.optional(cb);

Expand All @@ -33,11 +34,14 @@ module.exports = function(sails) {
if (err) { return cb(err); }

sails.log.verbose('Finished loading policy middleware logic.');
cb();
try {
this.bindPolicies();
} catch (e) {
return cb(e);
}
return cb();
}.bind(this));

// Before routing, curry controller functions with appropriate policy chains
sails.on('router:before', this.bindPolicies);
},

/**
Expand All @@ -47,12 +51,37 @@ module.exports = function(sails) {
* @api private
*/
loadMiddleware: function(cb) {
// Load policy modules
// Load policy modulesfrom disk.
sails.log.verbose('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(this.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(this.middleware, sails.config.policies.moduleDefinitions);
}

// Validate that all policies are functions.
try {
_.each(_.keys(this.middleware), function(policyName) {
// If we find a bad'n, bail out.
if (!_.isFunction(sails.hooks.policies.middleware[policyName])) {
throw 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(this.middleware, function(policyFn, policyName) {
policyFn._middlewareType = 'POLICY: '+policyName;
});

return cb();
}.bind(this));
},
Expand All @@ -63,74 +92,14 @@ module.exports = function(sails) {
* @api private
*/
bindPolicies: function() {

// Build / normalize policy config
this.mapping = this.buildPolicyMap();

// Bind a set of policies to a set of controllers
// (prepend policy chains to original middleware)
var mapping = this.mapping;
var middlewareSet = sails.middleware.controllers;
_.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) ) {
// sails.log.verbose('Applying policy (' + topLevelPolicyId + ') to controller\'s (' + id + ') actions...');
_.each(actions, function(actionId) {
actionFn = controller[actionId];
controller[actionId] = topLevelPolicyId.concat([actionFn]);
// sails.log.verbose('Applying policy to ' + id + '.' + actionId + '...', controller[actionId]);
});
return;
}

// :: Controller is a function
// -> apply the policy directly
// sails.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];
// sails.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['*'];
}
// sails.log.verbose('Applying policy (' + actionPolicy + ') to action (' + id + '.' + actionId + ')...');

actionFn = controller[actionId];
controller[actionId] = actionPolicy.concat([actionFn]);
});
}
});//</each in middlewareSet>
// 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.verbose('Policy-controller bindings complete!');
Expand All @@ -146,92 +115,105 @@ module.exports = function(sails) {
* @api private
*/
buildPolicyMap: function () {
var mapping = { };
_.each(sails.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) {
// 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();

// Case-insensitive
actionId = actionId.toLowerCase();
// Declare a "never allow" function to use when a policy of `false` is encountered.
var neverAllow = function neverAllow (req, res, next) {
res.forbidden();
};
neverAllow._middlewareType = 'POLICY: neverAllow';

mapping[controllerId][actionId] = policyHookDef.normalizePolicy(__policy);
});
});
// Loop through the keys and create the map.
var mapping = _.reduce(actionsToProtect, function (memo, target, index) {

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);
}));
}
// Make sure policies are contained in an array.
if (!_.isArray(sails.config.policies[target])) {
throw new Error('Invalid policy setting for `' + target + '`: policies must be specified as an array!');
}

// 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];
}
// 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 new Error('Invalid policy setting for `' + target + '`: if `true` is specified, it must be the only policy in the array.');
}
// Map `false` to the "never allow" policy.
return undefined;
}
// 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 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 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 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 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.
if (target.slice(-2) === '.*') {
// Get a version of the target without the .*
var nakedTarget = target.slice(0,-2);
// Get a version of the target without the .
var dotTarget = 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 "dotTarget", 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(dotTarget) === 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;
}
}
}

// 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];
}
// Transform the allow/deny list into a comma-delimited string that can be
// understood by `registerActionMiddleware`.
memo[allowDenyList.join(',')] = policies;

// A false or null policy means NEVER allow any requests
if (policy === false || policy === null) {
var neverAllow = function neverAllow (req, res, next) {
res.forbidden();
};
neverAllow._middlewareType = 'POLICY: neverAllow';
return [neverAllow];
}
return memo;

// 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
sails.log.error('Cannot map invalid policy: ', policy);
return [function(req, res) {
throw new Error('Invalid policy: ' + policy);
}];
return mapping;
},

/**
Expand All @@ -244,28 +226,19 @@ module.exports = function(sails) {
return;
}

// Bind policy function to route
var fn = this.lookupFn(event.target.policy, 'config.routes');
sails.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.toLowerCase();
var policyId = event.target.policy.toLowerCase();

// Policy doesn't exist
if (!this.middleware[policyId] ) {
return Err.fatal.__UnknownPolicy__ (policyId, referencedIn, sails.config.paths.policies);
}

// Policy found
return this.middleware[policyId];
// Bind policy function to route
var fn = this.middleware[policyId];

sails.router.bind(event.path, fn, event.verb, _.merge(event.options, event.target));
}

};

/**
Expand Down
Loading

0 comments on commit 66d2b2d

Please sign in to comment.