From 1effc4e772168b8046aebfb0c7b146e795ea64ee Mon Sep 17 00:00:00 2001 From: Jacob Gable Date: Sat, 8 Jun 2013 18:39:24 -0500 Subject: [PATCH] Implement a permissable interface on models Added checks to the canThis process for a `permissable()` function that would allow Models to override the permissions process. --- core/shared/models/post.js | 36 ++++++ core/shared/permissions/index.js | 65 ++++++---- core/shared/permissions/objectTypeModelMap.js | 11 ++ core/test/ghost/permissions_spec.js | 117 ++++++++++++++---- 4 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 core/shared/permissions/objectTypeModelMap.js diff --git a/core/shared/models/post.js b/core/shared/models/post.js index a57935a0b6..8095c53d8c 100644 --- a/core/shared/models/post.js +++ b/core/shared/models/post.js @@ -5,6 +5,7 @@ var Post, Posts, _ = require('underscore'), + when = require('when'), Showdown = require('showdown'), converter = new Showdown.converter(), User = require('./user').User, @@ -134,6 +135,41 @@ }; }); }); + }, + + permissable: function (postModelOrId, userId, action_type, userPermissions) { + var self = this, + hasPermission, + postModel = postModelOrId; + + // If we passed in an id instead of a model, get the model + // then check the permissions + if (_.isNumber(postModelOrId) || _.isString(postModelOrId)) { + return this.read({id: postModelOrId}).then(function (foundPostModel) { + return self.permissable(foundPostModel, userId, action_type, userPermissions); + }); + } + + // TODO: This logic is temporary, will probably need to be updated + + hasPermission = _.any(userPermissions, function (perm) { + if (perm.get('object_type') !== 'post') { + return false; + } + + // True, if no object_id specified, or it matches + return !perm.get('object_id') || perm.get('object_id') === postModel.id; + }); + + // If this is the author of the post, allow it. + hasPermission = hasPermission || userId === postModel.get('author_id'); + + if (hasPermission) { + return when.resolve(); + } + + // Otherwise, you shall not pass. + return when.reject(); } }); diff --git a/core/shared/permissions/index.js b/core/shared/permissions/index.js index 422138bbed..bdb280e5c6 100644 --- a/core/shared/permissions/index.js +++ b/core/shared/permissions/index.js @@ -7,6 +7,7 @@ var _ = require('underscore'), when = require('when'), Models = require('../models'), + objectTypeModelMap = require('./objectTypeModelMap'), UserProvider = Models.User, PermissionsProvider = Models.Permission, init, @@ -20,12 +21,13 @@ this.userPermissionLoad = false; }; - CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type) { + CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type, userId) { var self = this, obj_type_handlers = {}; // Iterate through the object types, i.e. ['post', 'tag', 'user'] _.each(obj_types, function (obj_type) { + var TargetModel = objectTypeModelMap[obj_type]; // Create the 'handler' for the object type; // the '.post()' in canThis(user).edit.post() @@ -42,35 +44,51 @@ // Wait for the user loading to finish return self.userPermissionLoad.then(function (userPermissions) { - // Iterate through the user permissions looking for an affirmation - var hasPermission = _.any(userPermissions, function (userPermission) { - var permObjId; + var hasPermission; - // Look for a matching action type and object type first - if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) { - return false; - } + // Allow for a target model to implement a "Permissable" interface + if (TargetModel && _.isFunction(TargetModel.permissable)) { + return TargetModel.permissable(modelId, userId, act_type, userPermissions); + } - // Grab the object id (if specified, could be null) - permObjId = userPermission.get('object_id'); + // Otherwise, check all the permissions for matching object id + hasPermission = _.any(userPermissions, function (userPermission) { + var permObjId; - // If we didn't specify a model (any thing) - // or the permission didn't have an id scope set - // then the user has permission - if (!modelId || !permObjId) { - return true; - } + // Look for a matching action type and object type first + if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) { + return false; + } - // Otherwise, check if the id's match - // TODO: String vs Int comparison possibility here? - return modelId === permObjId; - }); + // Grab the object id (if specified, could be null) + permObjId = userPermission.get('object_id'); + + // If we didn't specify a model (any thing) + // or the permission didn't have an id scope set + // then the user has permission + if (!modelId || !permObjId) { + return true; + } + + // Otherwise, check if the id's match + // TODO: String vs Int comparison possibility here? + return modelId === permObjId; + }); if (hasPermission) { return when.resolve(); } + return when.reject(); + }).otherwise(function() { + // No permissions loaded, or error loading permissions + + // Still check for permissable without permissions + if (TargetModel && _.isFunction(TargetModel.permissable)) { + return TargetModel.permissable(modelId, userId, act_type, []); + } + return when.reject(); }); }; @@ -80,12 +98,13 @@ }; CanThisResult.prototype.beginCheck = function (user) { - var self = this; + var self = this, + userId = user.id || user; // TODO: Switch logic based on object type; user, role, post. // Kick off the fetching of the user data - this.userPermissionLoad = UserProvider.effectivePermissions(user.id || user); + this.userPermissionLoad = UserProvider.effectivePermissions(userId); // Iterate through the actions and their related object types // We should have loaded these through a permissions.init() call previously @@ -93,7 +112,7 @@ _.each(exported.actionsMap, function (obj_types, act_type) { // Build up the object type handlers; // the '.post()' parts in canThis(user).edit.post() - var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type); + var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type, userId); // Define a property for the action on the result; // the '.edit' in canThis(user).edit.post() diff --git a/core/shared/permissions/objectTypeModelMap.js b/core/shared/permissions/objectTypeModelMap.js new file mode 100644 index 0000000000..2d56251701 --- /dev/null +++ b/core/shared/permissions/objectTypeModelMap.js @@ -0,0 +1,11 @@ +(function () { + "use strict"; + + module.exports = { + 'post': require('../models/post').Post, + 'role': require('../models/role').Role, + 'user': require('../models/user').User, + 'permission': require('../models/permission').Permission, + 'setting': require('../models/setting').Setting + }; +}()); \ No newline at end of file diff --git a/core/test/ghost/permissions_spec.js b/core/test/ghost/permissions_spec.js index 021d42694d..0b8cda6b67 100644 --- a/core/test/ghost/permissions_spec.js +++ b/core/test/ghost/permissions_spec.js @@ -6,12 +6,14 @@ var _ = require("underscore"), when = require('when'), should = require('should'), + sinon = require('sinon'), errors = require('../../shared/errorHandling'), helpers = require('./helpers'), permissions = require('../../shared/permissions'), Models = require('../../shared/models'), UserProvider = Models.User, - PermissionsProvider = Models.Permission; + PermissionsProvider = Models.Permission, + PostProvider = Models.Post; describe('permissions', function () { @@ -33,6 +35,21 @@ { act: "remove", obj: "user" } ], currTestPermId = 1, + currTestUserId = 1, + createTestUser = function (email_address) { + if (!email_address) { + currTestUserId += 1; + email_address = "test" + currTestPermId + "@test.com"; + } + + var newUser = { + id: currTestUserId, + email_address: email_address, + password: "testing123" + }; + + return UserProvider.add(newUser); + }, createPermission = function (name, act, obj) { if (!name) { currTestPermId += 1; @@ -129,29 +146,33 @@ description: "test2 description" }); - testRole.save().then(function () { - return testRole.load('permissions'); - }).then(function () { - var rolePermission = new Models.Permission({ - name: "test edit posts", - action_type: 'edit', - object_type: 'post' + testRole.save() + .then(function () { + return testRole.load('permissions'); + }) + .then(function () { + var rolePermission = new Models.Permission({ + name: "test edit posts", + action_type: 'edit', + object_type: 'post' + }); + + testRole.related('permissions').length.should.equal(0); + + return rolePermission.save().then(function () { + return testRole.permissions().attach(rolePermission); + }); + }) + .then(function () { + return Models.Role.read({id: testRole.id}, { withRelated: ['permissions']}); + }) + .then(function (updatedRole) { + should.exist(updatedRole); + + updatedRole.related('permissions').length.should.equal(1); + + done(); }); - - testRole.related('permissions').length.should.equal(0); - - return rolePermission.save().then(function () { - return testRole.permissions().attach(rolePermission); - }); - }).then(function () { - return Models.Role.read({id: testRole.id}, { withRelated: ['permissions']}); - }).then(function (updatedRole) { - should.exist(updatedRole); - - updatedRole.related('permissions').length.should.equal(1); - - done(); - }); }); it('does not allow edit post without permission', function (done) { @@ -221,6 +242,56 @@ }); }); + it('can use permissable function on Model to allow something', function (done) { + var testUser, + permissableStub = sinon.stub(PostProvider, 'permissable', function () { + return when.resolve(); + }); + + createTestUser() + .then(function (createdTestUser) { + testUser = createdTestUser; + + return permissions.canThis(testUser).edit.post(123); + }) + .then(function () { + permissableStub.restore(); + + permissableStub.calledWith(123, testUser.id, 'edit').should.equal(true); + + done(); + }) + .otherwise(function () { + permissableStub.restore(); + errors.logError(new Error("Did not allow testUser")); + }); + }); + + it('can use permissable function on Model to forbid something', function (done) { + var testUser, + permissableStub = sinon.stub(PostProvider, 'permissable', function () { + return when.reject(); + }); + + createTestUser() + .then(function (createdTestUser) { + testUser = createdTestUser; + + return permissions.canThis(testUser).edit.post(123); + }) + .then(function () { + permissableStub.restore(); + + errors.logError(new Error("Allowed testUser to edit post")); + }) + .otherwise(function () { + permissableStub.restore(); + permissableStub.calledWith(123, testUser.id, 'edit').should.equal(true); + + done(); + }); + }); + }); }()); \ No newline at end of file