Skip to content

Commit

Permalink
Permissions: minor refactors (#9104)
Browse files Browse the repository at this point in the history
refs #9043

- Cleanups / refactors to make the code more manageable
- Move remaining code out of index.js 
   - Only "init" function is left. Actions map cache and init function is based heavily on the settings cache module
- refactor the odd way of exporting
   - This was cleaned up naturally by moving the actionsMap object out
- rename "effective" -> "providers"
  - "Providers" provide permissions for different things that can have permissions (users, apps, in future clients).
  • Loading branch information
ErisDS authored and kirrg001 committed Oct 5, 2017
1 parent c50415d commit 6760ccc
Show file tree
Hide file tree
Showing 7 changed files with 853 additions and 775 deletions.
43 changes: 43 additions & 0 deletions core/server/permissions/actions-map-cache.js
@@ -0,0 +1,43 @@
// Based heavily on the settings cache
var _ = require('lodash'),
actionsMap = {};

module.exports = {
getAll: function getAll() {
return _.cloneDeep(actionsMap);
},
init: function init(perms) {
var seenActions = {};

actionsMap = {};

// Build a hash map of the actions on objects, i.e
/*
{
'edit': ['post', 'tag', 'user', 'page'],
'delete': ['post', 'user'],
'create': ['post', 'user', 'page']
}
*/
_.each(perms.models, function (perm) {
var actionType = perm.get('action_type'),
objectType = perm.get('object_type');

actionsMap[actionType] = actionsMap[actionType] || [];
seenActions[actionType] = seenActions[actionType] || {};

// Check if we've already seen this action -> object combo
if (seenActions[actionType][objectType]) {
return;
}

actionsMap[actionType].push(objectType);
seenActions[actionType][objectType] = true;
});

return actionsMap;
},
empty: function empty() {
return _.size(actionsMap) === 0;
}
};
175 changes: 175 additions & 0 deletions core/server/permissions/can-this.js
@@ -0,0 +1,175 @@
var _ = require('lodash'),
Promise = require('bluebird'),
models = require('../models'),
errors = require('../errors'),
i18n = require('../i18n'),
providers = require('./providers'),
parseContext = require('./parse-context'),
actionsMap = require('./actions-map-cache'),
canThis,
CanThisResult;

// Base class for canThis call results
CanThisResult = function () {
return;
};

CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, context, permissionLoad) {
var objectTypeModelMap = {
post: models.Post,
role: models.Role,
user: models.User,
permission: models.Permission,
setting: models.Settings,
subscriber: models.Subscriber,
invite: models.Invite
};

// Iterate through the object types, i.e. ['post', 'tag', 'user']
return _.reduce(objTypes, function (objTypeHandlers, objType) {
// Grab the TargetModel through the objectTypeModelMap
var TargetModel = objectTypeModelMap[objType];

// Create the 'handler' for the object type;
// the '.post()' in canThis(user).edit.post()
objTypeHandlers[objType] = function (modelOrId, unsafeAttrs) {
var modelId;
unsafeAttrs = unsafeAttrs || {};

// If it's an internal request, resolve immediately
if (context.internal) {
return Promise.resolve();
}

if (_.isNumber(modelOrId) || _.isString(modelOrId)) {
// It's an id already, do nothing
modelId = modelOrId;
} else if (modelOrId) {
// It's a model, get the id
modelId = modelOrId.id;
}
// Wait for the user loading to finish
return permissionLoad.then(function (loadedPermissions) {
// Iterate through the user permissions looking for an affirmation
var userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null,
appPermissions = loadedPermissions.app ? loadedPermissions.app.permissions : null,
hasUserPermission,
hasAppPermission,
checkPermission = function (perm) {
var permObjId;

// Look for a matching action type and object type first
if (perm.get('action_type') !== actType || perm.get('object_type') !== objType) {
return false;
}

// Grab the object id (if specified, could be null)
permObjId = perm.get('object_id');

// If we didn't specify a model (any thing)
// or the permission didn't have an id scope set
// then the "thing" has permission
if (!modelId || !permObjId) {
return true;
}

// Otherwise, check if the id's match
// TODO: String vs Int comparison possibility here?
return modelId === permObjId;
};

if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'})) {
hasUserPermission = true;
} else if (!_.isEmpty(userPermissions)) {
hasUserPermission = _.some(userPermissions, checkPermission);
}

// Check app permissions if they were passed
hasAppPermission = true;
if (!_.isNull(appPermissions)) {
hasAppPermission = _.some(appPermissions, checkPermission);
}

// Offer a chance for the TargetModel to override the results
if (TargetModel && _.isFunction(TargetModel.permissible)) {
return TargetModel.permissible(
modelId, actType, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission
);
}

if (hasUserPermission && hasAppPermission) {
return;
}

return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.permissions.noPermissionToAction')}));
});
};

return objTypeHandlers;
}, {});
};

CanThisResult.prototype.beginCheck = function (context) {
var self = this,
userPermissionLoad,
appPermissionLoad,
permissionsLoad;

// Get context.user and context.app
context = parseContext(context);

if (actionsMap.empty()) {
throw new Error(i18n.t('errors.permissions.noActionsMapFound.error'));
}

// Kick off loading of user permissions if necessary
if (context.user) {
userPermissionLoad = providers.user(context.user);
} else {
// Resolve null if no context.user to prevent db call
userPermissionLoad = Promise.resolve(null);
}

// Kick off loading of app permissions if necessary
if (context.app) {
appPermissionLoad = providers.app(context.app);
} else {
// Resolve null if no context.app
appPermissionLoad = Promise.resolve(null);
}

// Wait for both user and app permissions to load
permissionsLoad = Promise.all([userPermissionLoad, appPermissionLoad]).then(function (result) {
return {
user: result[0],
app: result[1]
};
});

// Iterate through the actions and their related object types
_.each(actionsMap.getAll(), function (objTypes, actType) {
// Build up the object type handlers;
// the '.post()' parts in canThis(user).edit.post()
var objTypeHandlers = self.buildObjectTypeHandlers(objTypes, actType, context, permissionsLoad);

// Define a property for the action on the result;
// the '.edit' in canThis(user).edit.post()
Object.defineProperty(self, actType, {
writable: false,
enumerable: false,
configurable: false,
value: objTypeHandlers
});
});

// Return this for chaining
return this;
};

canThis = function (context) {
var result = new CanThisResult();

return result.beginCheck(context);
};

module.exports = canThis;

0 comments on commit 6760ccc

Please sign in to comment.