From 524b247c58e5fc6894cf3add78fa54051608fdd9 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sat, 27 Jun 2015 19:09:25 +0100 Subject: [PATCH] Add public API permission handling refs #4004, #5614 - added new public permission handling functions to permissions - added a new util to handle either public permissions or normal permissions - updated posts, tags and users endpoints to use the new util - added test coverage for the new code --- core/server/api/posts.js | 31 +-- core/server/api/tags.js | 32 +-- core/server/api/users.js | 33 +-- core/server/api/utils.js | 66 ++++- core/server/permissions/index.js | 66 ++++- core/test/integration/api/api_posts_spec.js | 48 ++-- core/test/integration/api/api_users_spec.js | 38 ++- core/test/unit/api_utils_spec.js | 83 +++++- core/test/unit/permissions_spec.js | 265 ++++++++++++++++++-- 9 files changed, 504 insertions(+), 158 deletions(-) diff --git a/core/server/api/posts.js b/core/server/api/posts.js index d8a2170a34..e7c5a76ccd 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -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 @@ -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 ]; @@ -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 @@ -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 ]; diff --git a/core/server/api/tags.js b/core/server/api/tags.js index 11465baf2d..7eff5314ba 100644 --- a/core/server/api/tags.js +++ b/core/server/api/tags.js @@ -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 @@ -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 ]; @@ -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 @@ -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 ]; diff --git a/core/server/api/users.js b/core/server/api/users.js index 782f9d5edd..b88ca5425a 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -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 @@ -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 ]; @@ -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; } /** @@ -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 ]; diff --git a/core/server/api/utils.js b/core/server/api/utils.js index 6944ddb66b..566a0cf6d8 100644 --- a/core/server/api/utils.js +++ b/core/server/api/utils.js @@ -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 = { @@ -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 */ diff --git a/core/server/permissions/index.js b/core/server/permissions/index.js index 86e49b6a71..0dc9d6c7e0 100644 --- a/core/server/permissions/index.js +++ b/core/server/permissions/index.js @@ -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; @@ -244,5 +304,7 @@ module.exports = exported = { init: init, refresh: refresh, canThis: canThis, + parseContext: parseContext, + applyPublicRules: applyPublicRules, actionsMap: {} }; diff --git a/core/test/integration/api/api_posts_spec.js b/core/test/integration/api/api_posts_spec.js index 8e6bd879f0..255e2fd486 100644 --- a/core/test/integration/api/api_posts_spec.js +++ b/core/test/integration/api/api_posts_spec.js @@ -126,15 +126,35 @@ describe('Post API', function () { it('without context.user cannot fetch all posts', function (done) { PostAPI.browse({status: 'all'}).then(function (results) { - should.exist(results); - testUtils.API.checkResponse(results, 'posts'); - should.exist(results.posts); - results.posts.length.should.eql(4); - results.posts[0].status.should.eql('published'); - testUtils.API.checkResponse(results.posts[0], 'post'); + should.not.exist(results); + done(new Error('should not provide results if invalid status provided')); + }).catch(function (err) { + err.errorType.should.eql('NoPermissionError'); done(); - }).catch(done); + }); + }); + + it('without context.user cannot fetch draft posts', function (done) { + PostAPI.browse({status: 'draft'}).then(function (results) { + should.not.exist(results); + + done(new Error('should not provide results if invalid status provided')); + }).catch(function (err) { + err.errorType.should.eql('NoPermissionError'); + done(); + }); + }); + + it('without context.user cannot use uuid to fetch draft posts in browse', function (done) { + PostAPI.browse({status: 'draft', uuid: 'imastring'}).then(function (results) { + should.not.exist(results); + + done(new Error('should not provide results if invalid status provided')); + }).catch(function (err) { + err.errorType.should.eql('NoPermissionError'); + done(); + }); }); it('with context.user can fetch drafts', function (done) { @@ -230,15 +250,13 @@ describe('Post API', function () { }); it('without context.user cannot fetch draft', function (done) { - PostAPI.read({slug: 'unfinished', status: 'draft'}).then(function (results) { - should.not.exist(results.posts); - done(); + PostAPI.read({slug: 'unfinished', status: 'draft'}).then(function () { + done(new Error('Should not return a result with no permission')); }).catch(function (err) { should.exist(err); - err.message.should.eql('Post not found.'); - + err.errorType.should.eql('NoPermissionError'); done(); - }); + }).catch(done); }); it('with context.user can fetch a draft', function (done) { @@ -260,13 +278,13 @@ describe('Post API', function () { it('cannot fetch post with unknown id', function (done) { PostAPI.read({context: {user: 1}, slug: 'not-a-post'}).then(function () { - done(); + done(new Error('Should not return a result with unknown id')); }).catch(function (err) { should.exist(err); err.message.should.eql('Post not found.'); done(); - }); + }).catch(done); }); it('can fetch post with by id', function (done) { diff --git a/core/test/integration/api/api_users_spec.js b/core/test/integration/api/api_users_spec.js index 096566e728..9a4a5f7135 100644 --- a/core/test/integration/api/api_users_spec.js +++ b/core/test/integration/api/api_users_spec.js @@ -49,15 +49,15 @@ describe('Users API', function () { }); describe('Browse', function () { - function checkBrowseResponse(response, count) { + function checkBrowseResponse(response, count, additional, missing) { should.exist(response); testUtils.API.checkResponse(response, 'users'); should.exist(response.users); response.users.should.have.length(count); - testUtils.API.checkResponse(response.users[0], 'user'); - testUtils.API.checkResponse(response.users[1], 'user'); - testUtils.API.checkResponse(response.users[2], 'user'); - testUtils.API.checkResponse(response.users[3], 'user'); + testUtils.API.checkResponse(response.users[0], 'user', additional, missing); + testUtils.API.checkResponse(response.users[1], 'user', additional, missing); + testUtils.API.checkResponse(response.users[2], 'user', additional, missing); + testUtils.API.checkResponse(response.users[3], 'user', additional, missing); } it('Owner can browse', function (done) { @@ -88,9 +88,16 @@ describe('Users API', function () { }).catch(done); }); - it('No-auth CANNOT browse', function (done) { - UserAPI.browse().then(function () { - done(new Error('Browse users is not denied without authentication.')); + it('No-auth CAN browse, but only gets filtered active users', function (done) { + UserAPI.browse().then(function (response) { + checkBrowseResponse(response, 7, null, ['email']); + done(); + }).catch(done); + }); + + it('No-auth CANNOT browse non-active users', function (done) { + UserAPI.browse({status: 'invited'}).then(function () { + done(new Error('Browse non-active users is not denied without authentication.')); }, function () { done(); }).catch(done); @@ -111,21 +118,6 @@ describe('Users API', function () { }); }); - it('Author can browse', function (done) { - UserAPI.browse(context.author).then(function (response) { - checkBrowseResponse(response, 7); - done(); - }).catch(done); - }); - - it('No-auth CANNOT browse', function (done) { - UserAPI.browse().then(function () { - done(new Error('Browse users is not denied without authentication.')); - }, function () { - done(); - }).catch(done); - }); - it('Can browse all', function (done) { UserAPI.browse(_.extend({}, testUtils.context.admin, {status: 'all'})).then(function (response) { checkBrowseResponse(response, 7); diff --git a/core/test/unit/api_utils_spec.js b/core/test/unit/api_utils_spec.js index 9381cf7f91..ad1e0ff759 100644 --- a/core/test/unit/api_utils_spec.js +++ b/core/test/unit/api_utils_spec.js @@ -1,19 +1,16 @@ -/*globals describe, it, beforeEach, afterEach */ +/*globals describe, it, afterEach */ /*jshint expr:true*/ var should = require('should'), sinon = require('sinon'), _ = require('lodash'), Promise = require('bluebird'), - apiUtils = require('../../server/api/utils'); + permissions = require('../../server/permissions'), + apiUtils = require('../../server/api/utils'), + + sandbox = sinon.sandbox.create(); describe('API Utils', function () { - var sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - afterEach(function () { sandbox.restore(); }); @@ -405,4 +402,74 @@ describe('API Utils', function () { apiUtils.checkFileIsValid({name: 'test.txt', type: 'text'}, ['archive'], ['.txt']).should.be.false; }); }); + + describe('isPublicContext', function () { + it('should call out to permissions', function () { + var permsStub = sandbox.stub(permissions, 'parseContext').returns({public: true}); + apiUtils.isPublicContext({context: 'test'}).should.be.true; + permsStub.called.should.be.true; + permsStub.calledWith('test').should.be.true; + }); + }); + + describe('applyPublicPermissions', function () { + it('should call out to permissions', function () { + var permsStub = sandbox.stub(permissions, 'applyPublicRules'); + apiUtils.applyPublicPermissions('test', {}); + permsStub.called.should.be.true; + permsStub.calledWith('test', {}).should.be.true; + }); + }); + + describe('handlePublicPermissions', function () { + it('should return empty options if passed empty options', function (done) { + apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) { + options.should.eql({}); + done(); + }).catch(done); + }); + + it('should treat no context as public', function (done) { + var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({})); + apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) { + aPPStub.calledOnce.should.eql(true); + options.should.eql({}); + done(); + }).catch(done); + }); + + it('should treat user context as NOT public', function (done) { + var cTMethodStub = { + test: { + test: sandbox.stub().returns(Promise.resolve()) + } + }, + cTStub = sandbox.stub(permissions, 'canThis').returns(cTMethodStub); + + apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function (options) { + cTStub.calledOnce.should.eql(true); + cTMethodStub.test.test.calledOnce.should.eql(true); + options.should.eql({context: {user: 1}}); + done(); + }).catch(done); + }); + + it('should throw a permissions error if permission is not granted', function (done) { + var cTMethodStub = { + test: { + test: sandbox.stub().returns(Promise.reject()) + } + }, + cTStub = sandbox.stub(permissions, 'canThis').returns(cTMethodStub); + + apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function () { + done(new Error('should throw error when no permissions')); + }).catch(function (err) { + cTStub.calledOnce.should.eql(true); + cTMethodStub.test.test.calledOnce.should.eql(true); + err.errorType.should.eql('NoPermissionError'); + done(); + }).catch(done); + }); + }); }); diff --git a/core/test/unit/permissions_spec.js b/core/test/unit/permissions_spec.js index 062d4b2179..5bfe379bd8 100644 --- a/core/test/unit/permissions_spec.js +++ b/core/test/unit/permissions_spec.js @@ -14,39 +14,268 @@ var testUtils = require('../utils'), sandbox = sinon.sandbox.create(); -// TODO move to integrations or stub - describe('Permissions', function () { - before(function (done) { - Models.init().then(done).catch(done); - }); - afterEach(function () { sandbox.restore(); }); - beforeEach(function () { - var permissions = _.map(testUtils.DataGenerator.Content.permissions, function (testPerm) { - return testUtils.DataGenerator.forKnex.createPermission(testPerm); + describe('actions map', function () { + before(function (done) { + Models.init().then(done).catch(done); }); - sandbox.stub(Models.Permission, 'findAll', function () { - return Promise.resolve(Models.Permissions.forge(permissions)); + beforeEach(function () { + var permissions = _.map(testUtils.DataGenerator.Content.permissions, function (testPerm) { + return testUtils.DataGenerator.forKnex.createPermission(testPerm); + }); + + sandbox.stub(Models.Permission, 'findAll', function () { + return Promise.resolve(Models.Permissions.forge(permissions)); + }); + }); + + it('can load an actions map from existing permissions', function (done) { + permissions.init().then(function (actionsMap) { + should.exist(actionsMap); + + actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page'].sort()); + + actionsMap.should.equal(permissions.actionsMap); + + done(); + }).catch(done); }); }); - it('can load an actions map from existing permissions', function (done) { - permissions.init().then(function (actionsMap) { - should.exist(actionsMap); + describe('parseContext', function () { + it('should return public for no context', function () { + permissions.parseContext().should.eql({ + internal: false, + user: null, + app: null, + public: true + }); + permissions.parseContext({}).should.eql({ + internal: false, + user: null, + app: null, + public: true + }); + }); - actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page'].sort()); + it('should return public for random context', function () { + permissions.parseContext('public').should.eql({ + internal: false, + user: null, + app: null, + public: true + }); + permissions.parseContext({client: 'thing'}).should.eql({ + internal: false, + user: null, + app: null, + public: true + }); + }); - actionsMap.should.equal(permissions.actionsMap); + it('should return user if user populated', function () { + permissions.parseContext({user: 1}).should.eql({ + internal: false, + user: 1, + app: null, + public: false + }); + }); - done(); - }).catch(done); + it('should return app if app populated', function () { + permissions.parseContext({app: 5}).should.eql({ + internal: false, + user: null, + app: 5, + public: false + }); + }); + + it('should return internal if internal provided', function () { + permissions.parseContext({internal: true}).should.eql({ + internal: true, + user: null, + app: null, + public: false + }); + + permissions.parseContext('internal').should.eql({ + internal: true, + user: null, + app: null, + public: false + }); + }); }); + describe('applyPublicRules', function () { + it('should return empty object for docName with no rules', function (done) { + permissions.applyPublicRules('test', 'test', {}).then(function (result) { + result.should.eql({}); + done(); + }); + }); + + it('should return unchanged object for non-public context', function (done) { + var internal = {context: 'internal'}, + user = {context: {user: 1}}, + app = {context: {app: 1}}; + + permissions.applyPublicRules('posts', 'browse', _.cloneDeep(internal)).then(function (result) { + result.should.eql(internal); + + return permissions.applyPublicRules('posts', 'browse', _.cloneDeep(user)); + }).then(function (result) { + result.should.eql(user); + + return permissions.applyPublicRules('posts', 'browse', _.cloneDeep(app)); + }).then(function (result) { + result.should.eql(app); + + done(); + }).catch(done); + }); + + it('should return unchanged object for post with public context', function (done) { + var public = {context: {}}; + + permissions.applyPublicRules('posts', 'browse', _.cloneDeep(public)).then(function (result) { + result.should.not.eql(public); + result.should.eql({ + context: {}, + status: 'published' + }); + + return permissions.applyPublicRules('posts', 'browse', _.extend({}, _.cloneDeep(public), {status: 'published'})); + }).then(function (result) { + result.should.eql({ + context: {}, + status: 'published' + }); + + done(); + }).catch(done); + }); + + it('should throw an error for draft post without uuid (read)', function (done) { + var draft = {context: {}, data: {status: 'draft'}}; + + permissions.applyPublicRules('posts', 'read', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + + it('should throw an error for draft post (browse)', function (done) { + var draft = {context: {}, status: 'draft'}; + + permissions.applyPublicRules('posts', 'browse', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + + it('should permit post draft status with uuid (read)', function (done) { + var draft = {context: {}, data: {status: 'draft', uuid: '1234-abcd'}}; + + permissions.applyPublicRules('posts', 'read', _.cloneDeep(draft)).then(function (result) { + result.should.eql(draft); + done(); + }).catch(done); + }); + + it('should permit post all status with uuid (read)', function (done) { + var draft = {context: {}, data: {status: 'all', uuid: '1234-abcd'}}; + + permissions.applyPublicRules('posts', 'read', _.cloneDeep(draft)).then(function (result) { + result.should.eql(draft); + done(); + }).catch(done); + }); + + it('should NOT permit post draft status with uuid (browse)', function (done) { + var draft = {context: {}, status: 'draft', uuid: '1234-abcd'}; + + permissions.applyPublicRules('posts', 'browse', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + + it('should NOT permit post all status with uuid (browse)', function (done) { + var draft = {context: {}, status: 'all', uuid: '1234-abcd'}; + + permissions.applyPublicRules('posts', 'browse', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + + it('should throw an error for draft post with uuid and id or slug (read)', function (done) { + var draft = {context: {}, data: {status: 'draft', uuid: '1234-abcd', id: 1}}; + + permissions.applyPublicRules('posts', 'read', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + draft = {context: {}, data: {status: 'draft', uuid: '1234-abcd', slug: 'abcd'}}; + + return permissions.applyPublicRules('posts', 'read', _.cloneDeep(draft)).then(function () { + done('Did not throw an error for draft'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + }); + + it('should return unchanged object for user with public context', function (done) { + var public = {context: {}}; + + permissions.applyPublicRules('users', 'browse', _.cloneDeep(public)).then(function (result) { + result.should.not.eql(public); + result.should.eql({ + context: {}, + status: 'active' + }); + + return permissions.applyPublicRules('users', 'browse', _.extend({}, _.cloneDeep(public), {status: 'active'})); + }).then(function (result) { + result.should.eql({ + context: {}, + status: 'active' + }); + + done(); + }).catch(done); + }); + + it('should throw an error for an inactive user', function (done) { + var inactive = {context: {}, status: 'inactive'}; + + permissions.applyPublicRules('users', 'browse', _.cloneDeep(inactive)).then(function () { + done('Did not throw an error for inactive'); + }).catch(function (err) { + err.should.be.a.String; + done(); + }); + }); + }); + + // @TODO: move to integrations or stub // it('does not allow edit post without permission', function (done) { // var fakePage = { // id: 1