Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public API endpoint permission handling #5496

Merged
merged 1 commit into from
Aug 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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