diff --git a/core/server/api/utils.js b/core/server/api/utils.js index f7b5dfbae5af..a48ef98e3a4e 100644 --- a/core/server/api/utils.js +++ b/core/server/api/utils.js @@ -137,13 +137,14 @@ utils = { }, /** - * ## Is Public Context? - * If this is a public context, return true + * ## Detect Public Context + * Calls parse context to expand the options.context object * @param {Object} options * @returns {Boolean} */ - isPublicContext: function isPublicContext(options) { - return permissions.parseContext(options.context).public; + detectPublicContext: function detectPublicContext(options) { + options.context = permissions.parseContext(options.context); + return options.context.public; }, /** * ## Apply Public Permissions @@ -174,7 +175,7 @@ utils = { return function doHandlePublicPermissions(options) { var permsPromise; - if (utils.isPublicContext(options)) { + if (utils.detectPublicContext(options)) { permsPromise = utils.applyPublicPermissions(docName, method, options); } else { permsPromise = permissions.canThis(options.context)[method][singular](options.data); diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index b446f88e15a0..0f6b52e7286e 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -30,6 +30,9 @@ ghostBookshelf = bookshelf(config.database.knex); // Load the Bookshelf registry plugin, which helps us avoid circular dependencies ghostBookshelf.plugin('registry'); +// Load the Ghost access rules plugin, which handles passing permissions/context through the model layer +ghostBookshelf.plugin(plugins.accessRules); + // Load the Ghost include count plugin, which allows for the inclusion of cross-table counts ghostBookshelf.plugin(plugins.includeCount); @@ -268,7 +271,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ options = options || {}; var self = this, - itemCollection = this.forge(), + itemCollection = this.forge(null, {context: options.context}), tableName = _.result(this.prototype, 'tableName'); // Filter options so that only permitted ones remain diff --git a/core/server/models/plugins/access-rules.js b/core/server/models/plugins/access-rules.js new file mode 100644 index 000000000000..3a58f3ab514d --- /dev/null +++ b/core/server/models/plugins/access-rules.js @@ -0,0 +1,45 @@ +// # Access Rules +// +// Extends Bookshelf.Model.force to take a 'context' option which provides information on how this query should +// be treated in terms of data access rules - currently just detecting public requests +module.exports = function (Bookshelf) { + var model = Bookshelf.Model, + Model; + + Model = Bookshelf.Model.extend({ + /** + * Cached copy of the context setup for this model instance + */ + _context: null, + /** + * ## Is Public Context? + * A helper to determine if this is a public request or not + * @returns {boolean} + */ + isPublicContext: function isPublicContext() { + return !!(this._context && this._context.public); + } + }, + { + /** + * ## Forge + * Ensure that context gets set as part of the forge + * + * @param {object} attributes + * @param {object} options + * @returns {Bookshelf.Model} model + */ + forge: function forge(attributes, options) { + var self = model.forge.apply(this, arguments); + + if (options && options.context) { + self._context = options.context; + delete options.context; + } + + return self; + } + }); + + Bookshelf.Model = Model; +}; diff --git a/core/server/models/plugins/index.js b/core/server/models/plugins/index.js index 0c3a38d6124b..4347f0493b71 100644 --- a/core/server/models/plugins/index.js +++ b/core/server/models/plugins/index.js @@ -1,4 +1,5 @@ module.exports = { + accessRules: require('./access-rules'), includeCount: require('./include-count'), pagination: require('./pagination') }; diff --git a/core/test/unit/api_utils_spec.js b/core/test/unit/api_utils_spec.js index 183831288772..6ec4487058f5 100644 --- a/core/test/unit/api_utils_spec.js +++ b/core/test/unit/api_utils_spec.js @@ -406,7 +406,7 @@ describe('API Utils', function () { 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; + apiUtils.detectPublicContext({context: 'test'}).should.be.true; permsStub.called.should.be.true; permsStub.calledWith('test').should.be.true; }); @@ -424,7 +424,7 @@ describe('API Utils', function () { describe('handlePublicPermissions', function () { it('should return empty options if passed empty options', function (done) { apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) { - options.should.eql({}); + options.should.eql({context: {app: null, internal: false, public: true, user: null}}); done(); }).catch(done); }); @@ -433,7 +433,7 @@ describe('API Utils', function () { var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({})); apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) { aPPStub.calledOnce.should.eql(true); - options.should.eql({}); + options.should.eql({context: {app: null, internal: false, public: true, user: null}}); done(); }).catch(done); }); @@ -449,7 +449,7 @@ describe('API Utils', function () { 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}}); + options.should.eql({context: {app: null, internal: false, public: false, user: 1}}); done(); }).catch(done); }); diff --git a/core/test/unit/models_plugins/access-rules_spec.js b/core/test/unit/models_plugins/access-rules_spec.js new file mode 100644 index 000000000000..0bab419e2dc9 --- /dev/null +++ b/core/test/unit/models_plugins/access-rules_spec.js @@ -0,0 +1,54 @@ +/*globals describe, it, beforeEach, afterEach */ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + +// Thing we're testing + // accessRules = require('../../../server/models/plugins/access-rules'), + models = require('../../../server/models'), + ghostBookshelf, + + sandbox = sinon.sandbox.create(); + +// To stop jshint complaining +should.equal(true, true); + +describe('Access Rules', function () { + beforeEach(function () { + return models.init().then(function () { + ghostBookshelf = models.Base; + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Base Model', function () { + it('should assign isPublicContext to prototype', function () { + ghostBookshelf.Model.prototype.isPublicContext.should.be.a.Function; + }); + + it('should get called when a model is forged', function () { + ghostBookshelf.Model.forge(null, {context: 'test'})._context.should.eql('test'); + }); + + describe('isPublicContext', function () { + it('should isPublicContext false if no context is set', function () { + ghostBookshelf.Model.forge().isPublicContext().should.be.false; + }); + + it('should return false if context has no `public` property', function () { + ghostBookshelf.Model.forge(null, {context: 'test'}).isPublicContext().should.be.false; + }); + + it('should return false if context.public is false', function () { + ghostBookshelf.Model.forge(null, {context: {public: false}}).isPublicContext().should.be.false; + }); + + it('should return true if context.public is true', function () { + ghostBookshelf.Model.forge(null, {context: {public: true}}).isPublicContext().should.be.true; + }); + }); + }); +});