Ghost/core/server/permissions/index.js

322 lines
10 KiB
JavaScript
Raw Normal View History

// canThis(someUser).edit.posts([id]|[[ids]])
// canThis(someUser).edit.post(somePost|somePostId)
2014-02-05 12:40:30 +04:00
var _ = require('lodash'),
Promise = require('bluebird'),
errors = require('../errors'),
Models = require('../models'),
effectivePerms = require('./effective'),
i18n = require('../i18n'),
init,
refresh,
canThis,
CanThisResult,
exported;
2013-08-16 03:22:08 +04:00
function hasActionsMap() {
// Just need to find one key in the actionsMap
return _.some(exported.actionsMap, function (val, key) {
/*jslint unparam:true*/
return Object.hasOwnProperty.call(exported.actionsMap, key);
2013-08-16 03:22:08 +04:00
});
}
function parseContext(context) {
// Parse what's passed to canThis.beginCheck for standard user and app scopes
var parsed = {
internal: false,
external: false,
user: null,
app: null,
public: true
};
if (context && (context === 'external' || context.external)) {
parsed.external = true;
parsed.public = false;
}
if (context && (context === 'internal' || context.internal)) {
parsed.internal = true;
parsed.public = false;
}
if (context && context.user) {
parsed.user = context.user;
parsed.public = false;
}
if (context && context.app) {
parsed.app = context.app;
parsed.public = false;
}
return parsed;
}
function applyStatusRules(docName, method, opts) {
var err = new errors.NoPermissionError({message: i18n.t('errors.permissions.applyStatusRules.error', {docName: docName})});
// Enforce status 'active' for users
if (docName === 'users') {
if (!opts.status) {
return 'active';
} else if (opts.status !== 'active') {
🎨 configurable logging with bunyan (#7431) - 🛠 add bunyan and prettyjson, remove morgan - ✨ add logging module - GhostLogger class that handles setup of bunyan - PrettyStream for stdout - ✨ config for logging - @TODO: testing level fatal? - ✨ log each request via GhostLogger (express middleware) - @TODO: add errors to output - 🔥 remove errors.updateActiveTheme - we can read the value from config - 🔥 remove 15 helper functions in core/server/errors/index.js - all these functions get replaced by modules: 1. logging 2. error middleware handling for html/json 3. error creation (which will be part of PR #7477) - ✨ add express error handler for html/json - one true error handler for express responses - contains still some TODO's, but they are not high priority for first implementation/integration - this middleware only takes responsibility of either rendering html responses or return json error responses - 🎨 use new express error handler in middleware/index - 404 and 500 handling - 🎨 return error instead of error message in permissions/index.js - the rule for error handling should be: if you call a unit, this unit should return a custom Ghost error - 🎨 wrap serve static module - rule: if you call a module/unit, you should always wrap this error - it's always the same rule - so the caller never has to worry about what comes back - it's always a clear error instance - in this case: we return our notfounderror if serve static does not find the resource - this avoid having checks everywhere - 🎨 replace usages of errors/index.js functions and adapt tests - use logging.error, logging.warn - make tests green - remove some usages of logging and throwing api errors -> because when a request is involved, logging happens automatically - 🐛 return errorDetails to Ghost-Admin - errorDetails is used for Theme error handling - 🎨 use 500er error for theme is missing error in theme-handler - 🎨 extend file rotation to 1w
2016-10-04 18:33:43 +03:00
throw err;
}
}
// Enforce status 'published' for posts
if (docName === 'posts') {
if (!opts.status) {
return 'published';
} else if (
method === 'read'
&& (opts.status === 'draft' || opts.status === 'all')
&& _.isString(opts.uuid) && _.isUndefined(opts.id) && _.isUndefined(opts.slug)
) {
// public read requests can retrieve a draft, but only by UUID
return opts.status;
} else if (opts.status !== 'published') {
// any other parameter would make this a permissions error
🎨 configurable logging with bunyan (#7431) - 🛠 add bunyan and prettyjson, remove morgan - ✨ add logging module - GhostLogger class that handles setup of bunyan - PrettyStream for stdout - ✨ config for logging - @TODO: testing level fatal? - ✨ log each request via GhostLogger (express middleware) - @TODO: add errors to output - 🔥 remove errors.updateActiveTheme - we can read the value from config - 🔥 remove 15 helper functions in core/server/errors/index.js - all these functions get replaced by modules: 1. logging 2. error middleware handling for html/json 3. error creation (which will be part of PR #7477) - ✨ add express error handler for html/json - one true error handler for express responses - contains still some TODO's, but they are not high priority for first implementation/integration - this middleware only takes responsibility of either rendering html responses or return json error responses - 🎨 use new express error handler in middleware/index - 404 and 500 handling - 🎨 return error instead of error message in permissions/index.js - the rule for error handling should be: if you call a unit, this unit should return a custom Ghost error - 🎨 wrap serve static module - rule: if you call a module/unit, you should always wrap this error - it's always the same rule - so the caller never has to worry about what comes back - it's always a clear error instance - in this case: we return our notfounderror if serve static does not find the resource - this avoid having checks everywhere - 🎨 replace usages of errors/index.js functions and adapt tests - use logging.error, logging.warn - make tests green - remove some usages of logging and throwing api errors -> because when a request is involved, logging happens automatically - 🐛 return errorDetails to Ghost-Admin - errorDetails is used for Theme error handling - 🎨 use 500er error for theme is missing error in theme-handler - 🎨 extend file rotation to 1w
2016-10-04 18:33:43 +03:00
throw err;
}
}
return opts.status;
}
/**
* API Public Permission Rules
* This method enforces the rules for public requests
* @param {String} docName
* @param {String} method (read || browse)
* @param {Object} options
* @returns {Object} options
*/
function applyPublicRules(docName, method, options) {
try {
// If this is a public context
if (parseContext(options.context).public === true) {
if (method === 'browse') {
options.status = applyStatusRules(docName, method, options);
} else if (method === 'read') {
options.data.status = applyStatusRules(docName, method, options.data);
}
}
return Promise.resolve(options);
} catch (err) {
return Promise.reject(err);
}
}
// 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) {
var modelId;
// 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, 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);
2013-08-16 03:22:08 +04:00
if (!hasActionsMap()) {
throw new Error(i18n.t('errors.permissions.noActionsMapFound.error'));
2013-08-16 03:22:08 +04:00
}
// 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 = Promise.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 = 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(exported.actionsMap, 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);
};
init = refresh = function (options) {
options = options || {};
// Load all the permissions
return Models.Permission.findAll(options).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 actionType = perm.get('action_type'),
objectType = perm.get('object_type');
exported.actionsMap[actionType] = exported.actionsMap[actionType] || [];
seenActions[actionType] = seenActions[actionType] || {};
// Check if we've already seen this action -> object combo
if (seenActions[actionType][objectType]) {
return;
}
exported.actionsMap[actionType].push(objectType);
seenActions[actionType][objectType] = true;
});
return exported.actionsMap;
});
};
module.exports = exported = {
init: init,
refresh: refresh,
canThis: canThis,
parseContext: parseContext,
applyPublicRules: applyPublicRules,
actionsMap: {}
};