mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-14 18:52:05 +03:00
4e3b21b7da
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
245 lines
8.1 KiB
JavaScript
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: {}
|
|
};
|