0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Add access rules bookshelf plugin

refs #5614

- change isPublicContext to detectPublicContext
  - behaviour now expands the context object out
  - this is a bit of a sideeffect, but this is the simplest change
    that makes it possible to use the context in the model layer without
    significant wider changes
- add new access rules plugin
  - takes a context object as part of `forge()` & caches it on the model instance
  - provides helper functions for testing access rules later on
This commit is contained in:
Hannah Wolfe 2015-11-11 18:12:18 +00:00
parent 007c06fdc0
commit 666a616551
6 changed files with 114 additions and 10 deletions

View file

@ -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);

View file

@ -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

View file

@ -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;
};

View file

@ -1,4 +1,5 @@
module.exports = {
accessRules: require('./access-rules'),
includeCount: require('./include-count'),
pagination: require('./pagination')
};

View file

@ -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);
});

View file

@ -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;
});
});
});
});