Ghost/core/server/permissions/index.js
Hannah Wolfe 4e3b21b7da Permissions Improvements
refs #3083, #3096

In order to implement advanced permissions based on roles for specific
actions, we need to know
what role the current context user has and also what action we are
granting permissions for:
- Permissible gets passed the action type
- Effective permissions keeps the user role and eventually passes it to
  permissible
- Fixed spelling
- Still needs tests
2014-07-28 06:29:59 +01:00

245 lines
8.1 KiB
JavaScript

// canThis(someUser).edit.posts([id]|[[ids]])
// canThis(someUser).edit.post(somePost|somePostId)
var _ = require('lodash'),
when = require('when'),
Models = require('../models'),
effectivePerms = require('./effective'),
init,
refresh,
canThis,
CanThisResult,
exported;
function hasActionsMap() {
// Just need to find one key in the actionsMap
return _.any(exported.actionsMap, function (val, key) {
/*jslint unparam:true*/
return Object.hasOwnProperty.call(exported.actionsMap, key);
});
}
// TODO: Move this to its own file so others can use it?
function parseContext(context) {
// Parse what's passed to canThis.beginCheck for standard user and app scopes
var parsed = {
internal: false,
user: null,
app: null
};
if (context && (context === 'internal' || context.internal)) {
parsed.internal = true;
}
if (context && context.user) {
parsed.user = context.user;
}
if (context && context.app) {
parsed.app = context.app;
}
return parsed;
}
// Base class for canThis call results
CanThisResult = function () {
return;
};
CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type, context, permissionLoad) {
// @TODO: remove this lazy require
var objectTypeModelMap = require('./objectTypeModelMap');
// Iterate through the object types, i.e. ['post', 'tag', 'user']
return _.reduce(obj_types, function (obj_type_handlers, obj_type) {
// Grab the TargetModel through the objectTypeModelMap
var TargetModel = objectTypeModelMap[obj_type];
// Create the 'handler' for the object type;
// the '.post()' in canThis(user).edit.post()
obj_type_handlers[obj_type] = function (modelOrId) {
var modelId;
// If it's an internal request, resolve immediately
if (context.internal) {
return when.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') !== act_type || perm.get('object_type') !== obj_type) {
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;
};
// Check user permissions for matching action, object and id.
if (_.any(loadedPermissions.user.roles, { 'name': 'Owner' })) {
hasUserPermission = true;
} else if (!_.isEmpty(userPermissions)) {
hasUserPermission = _.any(userPermissions, checkPermission);
}
// Check app permissions if they were passed
hasAppPermission = true;
if (!_.isNull(appPermissions)) {
hasAppPermission = _.any(appPermissions, checkPermission);
}
// Offer a chance for the TargetModel to override the results
if (TargetModel && _.isFunction(TargetModel.permissible)) {
return TargetModel.permissible(
modelId, act_type, context, loadedPermissions, hasUserPermission, hasAppPermission
);
}
if (hasUserPermission && hasAppPermission) {
return when.resolve();
}
return when.reject();
});
};
return obj_type_handlers;
}, {});
};
CanThisResult.prototype.beginCheck = function (context) {
var self = this,
userPermissionLoad,
appPermissionLoad,
permissionsLoad;
// Get context.user and context.app
context = parseContext(context);
if (!hasActionsMap()) {
throw new Error("No actions map found, please call permissions.init() before use.");
}
// Kick off loading of effective user permissions if necessary
if (context.user) {
userPermissionLoad = effectivePerms.user(context.user);
} else {
// Resolve null if no context.user to prevent db call
userPermissionLoad = when.resolve(null);
}
// Kick off loading of effective app permissions if necessary
if (context.app) {
appPermissionLoad = effectivePerms.app(context.app);
} else {
// Resolve null if no context.app
appPermissionLoad = when.resolve(null);
}
// Wait for both user and app permissions to load
permissionsLoad = when.all([userPermissionLoad, appPermissionLoad]).then(function (result) {
return {
user: result[0],
app: result[1]
};
});
// Iterate through the actions and their related object types
_.each(exported.actionsMap, function (obj_types, act_type) {
// Build up the object type handlers;
// the '.post()' parts in canThis(user).edit.post()
var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type, context, permissionsLoad);
// Define a property for the action on the result;
// the '.edit' in canThis(user).edit.post()
Object.defineProperty(self, act_type, {
writable: false,
enumerable: false,
configurable: false,
value: obj_type_handlers
});
});
// Return this for chaining
return this;
};
canThis = function (context) {
var result = new CanThisResult();
return result.beginCheck(context);
};
init = refresh = function () {
// Load all the permissions
return Models.Permission.findAll().then(function (perms) {
var seenActions = {};
exported.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 action_type = perm.get('action_type'),
object_type = perm.get('object_type');
exported.actionsMap[action_type] = exported.actionsMap[action_type] || [];
seenActions[action_type] = seenActions[action_type] || {};
// Check if we've already seen this action -> object combo
if (seenActions[action_type][object_type]) {
return;
}
exported.actionsMap[action_type].push(object_type);
seenActions[action_type][object_type] = true;
});
return when(exported.actionsMap);
});
};
module.exports = exported = {
init: init,
refresh: refresh,
canThis: canThis,
actionsMap: {}
};