Skip to content

Commit

Permalink
Merge pull request TryGhost#5496 from ErisDS/api-public-perms
Browse files Browse the repository at this point in the history
Add public API endpoint permission handling
  • Loading branch information
sebgie committed Aug 4, 2015
2 parents f2c2d4f + 524b247 commit cfce197
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 158 deletions.
31 changes: 2 additions & 29 deletions core/server/api/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,6 @@ posts = {
permittedOptions = utils.browseDefaultOptions.concat(extraOptions),
tasks;

/**
* ### Handle Permissions
* We need to either be an authorised user, or only return published posts.
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (!(options.context && options.context.user)) {
options.status = 'published';
}

return options;
}

/**
* ### Model Query
* Make the call to the Model layer
Expand All @@ -65,7 +51,7 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: permittedOptions}),
handlePermissions,
utils.handlePublicPermissions(docName, 'browse'),
utils.convertOptions(allowedIncludes),
modelQuery
];
Expand All @@ -86,19 +72,6 @@ posts = {
var attrs = ['id', 'slug', 'status', 'uuid'],
tasks;

/**
* ### Handle Permissions
* We need to either be an authorised user, or only return published posts.
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (!options.data.uuid && !(options.context && options.context.user)) {
options.data.status = 'published';
}
return options;
}

/**
* ### Model Query
* Make the call to the Model layer
Expand All @@ -112,7 +85,7 @@ posts = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {attrs: attrs}),
handlePermissions,
utils.handlePublicPermissions(docName, 'read'),
utils.convertOptions(allowedIncludes),
modelQuery
];
Expand Down
32 changes: 2 additions & 30 deletions core/server/api/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,6 @@ tags = {
browse: function browse(options) {
var tasks;

/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).browse.tag().then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error, 'You do not have permission to browse tags.');
});
}

/**
* ### Model Query
* Make the call to the Model layer
Expand All @@ -53,7 +39,7 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: utils.browseDefaultOptions}),
handlePermissions,
utils.handlePublicPermissions(docName, 'browse'),
utils.convertOptions(allowedIncludes),
doQuery
];
Expand All @@ -71,20 +57,6 @@ tags = {
var attrs = ['id', 'slug'],
tasks;

/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).read.tag().then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error, 'You do not have permission to read tags.');
});
}

/**
* ### Model Query
* Make the call to the Model layer
Expand All @@ -98,7 +70,7 @@ tags = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {attrs: attrs}),
handlePermissions,
utils.handlePublicPermissions(docName, 'read'),
utils.convertOptions(allowedIncludes),
doQuery
];
Expand Down
33 changes: 5 additions & 28 deletions core/server/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,6 @@ users = {
permittedOptions = utils.browseDefaultOptions.concat(extraOptions),
tasks;

/**
* ### Handle Permissions
* We need to either be an authorised user, or only return published posts.
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).browse.user().then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error, 'You do not have permission to browse users.');
});
}

/**
* ### Model Query
* Make the call to the Model layer
Expand All @@ -104,7 +90,7 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: permittedOptions}),
handlePermissions,
utils.handlePublicPermissions(docName, 'browse'),
utils.convertOptions(allowedIncludes),
doQuery
];
Expand All @@ -122,18 +108,9 @@ users = {
var attrs = ['id', 'slug', 'status', 'email', 'role'],
tasks;

/**
* ### Handle Permissions
* Convert 'me' safely
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (options.data.id === 'me' && options.context && options.context.user) {
options.data.id = options.context.user;
}

return options;
// Special handling for id = 'me'
if (options.id === 'me' && options.context && options.context.user) {
options.id = options.context.user;
}

/**
Expand All @@ -149,7 +126,7 @@ users = {
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {attrs: attrs}),
handlePermissions,
utils.handlePublicPermissions(docName, 'read'),
utils.convertOptions(allowedIncludes),
doQuery
];
Expand Down
66 changes: 61 additions & 5 deletions core/server/api/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// # API Utils
// Shared helpers for working with the API
var Promise = require('bluebird'),
_ = require('lodash'),
path = require('path'),
errors = require('../errors'),
validation = require('../data/validation'),
var Promise = require('bluebird'),
_ = require('lodash'),
path = require('path'),
errors = require('../errors'),
permissions = require('../permissions'),
validation = require('../data/validation'),

utils;

utils = {
Expand Down Expand Up @@ -131,13 +133,67 @@ utils = {
return errors;
},

/**
* ## Is Public Context?
* If this is a public context, return true
* @param {Object} options
* @returns {Boolean}
*/
isPublicContext: function isPublicContext(options) {
return permissions.parseContext(options.context).public;
},
/**
* ## Apply Public Permissions
* Update the options object so that the rules reflect what is permitted to be retrieved from a public request
* @param {String} docName
* @param {String} method (read || browse)
* @param {Object} options
* @returns {Object} options
*/
applyPublicPermissions: function applyPublicPermissions(docName, method, options) {
return permissions.applyPublicRules(docName, method, options);
},

/**
* ## Handle Public Permissions
* @param {String} docName
* @param {String} method (read || browse)
* @returns {Function}
*/
handlePublicPermissions: function handlePublicPermissions(docName, method) {
var singular = docName.replace(/s$/, '');

/**
* Check if this is a public request, if so use the public permissions, otherwise use standard canThis
* @param {Object} options
* @returns {Object} options
*/
return function doHandlePublicPermissions(options) {
var permsPromise;

if (utils.isPublicContext(options)) {
permsPromise = utils.applyPublicPermissions(docName, method, options);
} else {
permsPromise = permissions.canThis(options.context)[method][singular](options.data);
}

return permsPromise.then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error);
});
};
},

prepareInclude: function prepareInclude(include, allowedIncludes) {
include = include || '';
include = _.intersection(include.split(','), allowedIncludes);

return include;
},

/**
* ## Convert Options
* @param {Array} allowedIncludes
* @returns {Function} doConversion
*/
Expand Down
66 changes: 64 additions & 2 deletions core/server/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,90 @@ function hasActionsMap() {
});
}

// 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
app: null,
public: true
};

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 errorMsg = 'You do not have permission to retrieve ' + docName + ' with that status';

// Enforce status 'active' for users
if (docName === 'users') {
if (!opts.status) {
return 'active';
} else if (opts.status !== 'active') {
throw errorMsg;
}
}

// 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
throw errorMsg;
}
}

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;
Expand Down Expand Up @@ -244,5 +304,7 @@ module.exports = exported = {
init: init,
refresh: refresh,
canThis: canThis,
parseContext: parseContext,
applyPublicRules: applyPublicRules,
actionsMap: {}
};
Loading

0 comments on commit cfce197

Please sign in to comment.