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.
This commit is contained in:
Jacob Gable 2013-06-08 18:39:24 -05:00
parent aa659d29b6
commit 1effc4e772
4 changed files with 183 additions and 46 deletions

View File

@ -5,6 +5,7 @@
var Post, var Post,
Posts, Posts,
_ = require('underscore'), _ = require('underscore'),
when = require('when'),
Showdown = require('showdown'), Showdown = require('showdown'),
converter = new Showdown.converter(), converter = new Showdown.converter(),
User = require('./user').User, 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();
} }
}); });

View File

@ -7,6 +7,7 @@
var _ = require('underscore'), var _ = require('underscore'),
when = require('when'), when = require('when'),
Models = require('../models'), Models = require('../models'),
objectTypeModelMap = require('./objectTypeModelMap'),
UserProvider = Models.User, UserProvider = Models.User,
PermissionsProvider = Models.Permission, PermissionsProvider = Models.Permission,
init, init,
@ -20,12 +21,13 @@
this.userPermissionLoad = false; this.userPermissionLoad = false;
}; };
CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type) { CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type, userId) {
var self = this, var self = this,
obj_type_handlers = {}; obj_type_handlers = {};
// Iterate through the object types, i.e. ['post', 'tag', 'user'] // Iterate through the object types, i.e. ['post', 'tag', 'user']
_.each(obj_types, function (obj_type) { _.each(obj_types, function (obj_type) {
var TargetModel = objectTypeModelMap[obj_type];
// Create the 'handler' for the object type; // Create the 'handler' for the object type;
// the '.post()' in canThis(user).edit.post() // the '.post()' in canThis(user).edit.post()
@ -42,35 +44,51 @@
// Wait for the user loading to finish // Wait for the user loading to finish
return self.userPermissionLoad.then(function (userPermissions) { return self.userPermissionLoad.then(function (userPermissions) {
// Iterate through the user permissions looking for an affirmation // Iterate through the user permissions looking for an affirmation
var hasPermission = _.any(userPermissions, function (userPermission) { var hasPermission;
var permObjId;
// Look for a matching action type and object type first // Allow for a target model to implement a "Permissable" interface
if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) { if (TargetModel && _.isFunction(TargetModel.permissable)) {
return false; return TargetModel.permissable(modelId, userId, act_type, userPermissions);
} }
// Grab the object id (if specified, could be null) // Otherwise, check all the permissions for matching object id
permObjId = userPermission.get('object_id'); hasPermission = _.any(userPermissions, function (userPermission) {
var permObjId;
// If we didn't specify a model (any thing) // Look for a matching action type and object type first
// or the permission didn't have an id scope set if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) {
// then the user has permission return false;
if (!modelId || !permObjId) { }
return true;
}
// Otherwise, check if the id's match // Grab the object id (if specified, could be null)
// TODO: String vs Int comparison possibility here? permObjId = userPermission.get('object_id');
return modelId === 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;
}
// Otherwise, check if the id's match
// TODO: String vs Int comparison possibility here?
return modelId === permObjId;
});
if (hasPermission) { if (hasPermission) {
return when.resolve(); 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(); return when.reject();
}); });
}; };
@ -80,12 +98,13 @@
}; };
CanThisResult.prototype.beginCheck = function (user) { 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. // TODO: Switch logic based on object type; user, role, post.
// Kick off the fetching of the user data // 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 // Iterate through the actions and their related object types
// We should have loaded these through a permissions.init() call previously // We should have loaded these through a permissions.init() call previously
@ -93,7 +112,7 @@
_.each(exported.actionsMap, function (obj_types, act_type) { _.each(exported.actionsMap, function (obj_types, act_type) {
// Build up the object type handlers; // Build up the object type handlers;
// the '.post()' parts in canThis(user).edit.post() // 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; // Define a property for the action on the result;
// the '.edit' in canThis(user).edit.post() // the '.edit' in canThis(user).edit.post()

View File

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

View File

@ -6,12 +6,14 @@
var _ = require("underscore"), var _ = require("underscore"),
when = require('when'), when = require('when'),
should = require('should'), should = require('should'),
sinon = require('sinon'),
errors = require('../../shared/errorHandling'), errors = require('../../shared/errorHandling'),
helpers = require('./helpers'), helpers = require('./helpers'),
permissions = require('../../shared/permissions'), permissions = require('../../shared/permissions'),
Models = require('../../shared/models'), Models = require('../../shared/models'),
UserProvider = Models.User, UserProvider = Models.User,
PermissionsProvider = Models.Permission; PermissionsProvider = Models.Permission,
PostProvider = Models.Post;
describe('permissions', function () { describe('permissions', function () {
@ -33,6 +35,21 @@
{ act: "remove", obj: "user" } { act: "remove", obj: "user" }
], ],
currTestPermId = 1, 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) { createPermission = function (name, act, obj) {
if (!name) { if (!name) {
currTestPermId += 1; currTestPermId += 1;
@ -129,29 +146,33 @@
description: "test2 description" description: "test2 description"
}); });
testRole.save().then(function () { testRole.save()
return testRole.load('permissions'); .then(function () {
}).then(function () { return testRole.load('permissions');
var rolePermission = new Models.Permission({ })
name: "test edit posts", .then(function () {
action_type: 'edit', var rolePermission = new Models.Permission({
object_type: 'post' 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) { 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();
});
});
}); });
}()); }());