Merge pull request #419 from jgable/postPermissions

Edit Post Permissions
This commit is contained in:
Hannah Wolfe 2013-08-18 12:11:55 -07:00
commit fd33b276a0
8 changed files with 121 additions and 38 deletions

View File

@ -54,13 +54,14 @@
// by `addSubview`, which will in-turn remove any // by `addSubview`, which will in-turn remove any
// children of those views, and so on. // children of those views, and so on.
removeSubviews: function () { removeSubviews: function () {
var i, l, children = this.subviews; var children = this.subviews;
if (!children) { if (!children) {
return this; return this;
} }
for (i = 0, l = children.length; i < l; i += 1) {
children[i].remove(); _(children).invoke("remove");
}
this.subviews = []; this.subviews = [];
return this; return this;
}, },
@ -72,6 +73,32 @@
this.removeSubviews(); this.removeSubviews();
} }
return Backbone.View.prototype.remove.apply(this, arguments); return Backbone.View.prototype.remove.apply(this, arguments);
},
// Used in API request fail handlers to parse a standard api error
// response json for the message to display
getRequestErrorMessage: function (request) {
var message;
// Can't really continue without a request
if (!request) {
return null;
}
// Seems like a sensible default
message = request.statusText;
// If a non 200 response
if (request.status !== 200) {
try {
// Try to parse out the error, or default to "Unknown"
message = request.responseJSON.error || "Unknown Error";
} catch (e) {
message = "The server returned an error (" + (request.status || "?") + ").";
}
}
return message;
} }
}); });

View File

@ -157,17 +157,20 @@
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
} }
var model = this.model; var view = this,
model = this.model;
this.savePost().then(function () { this.savePost().then(function () {
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'success', type: 'success',
message: 'Your post was saved as ' + model.get('status'), message: 'Your post was saved as ' + model.get('status'),
status: 'passive' status: 'passive'
}); });
}, function () { }, function (request) {
var message = view.getRequestErrorMessage(request) || model.validationError;
Ghost.notifications.addItem({ Ghost.notifications.addItem({
type: 'error', type: 'error',
message: model.validationError, message: message,
status: 'passive' status: 'passive'
}); });
}); });

View File

@ -15,6 +15,7 @@ var config = require('./../config'),
models = require('./server/models'), models = require('./server/models'),
plugins = require('./server/plugins'), plugins = require('./server/plugins'),
requireTree = require('./server/require-tree'), requireTree = require('./server/require-tree'),
permissions = require('./server/permissions'),
// Variables // Variables
appRoot = path.resolve(__dirname, '../'), appRoot = path.resolve(__dirname, '../'),
@ -124,9 +125,14 @@ Ghost.prototype.init = function () {
var self = this; var self = this;
return when.join(instance.dataProvider.init(), instance.getPaths()).then(function () { return when.join(instance.dataProvider.init(), instance.getPaths()).then(function () {
// Initialize plugins
return self.initPlugins(); return self.initPlugins();
}, errors.logAndThrowError).then(function () { }).then(function () {
// Initialize the settings cache
return self.updateSettingsCache(); return self.updateSettingsCache();
}).then(function () {
// Initialize the permissions actions and objects
return permissions.init();
}, errors.logAndThrowError); }, errors.logAndThrowError);
}; };

View File

@ -5,6 +5,8 @@ var Ghost = require('../ghost'),
_ = require('underscore'), _ = require('underscore'),
when = require('when'), when = require('when'),
errors = require('./errorHandling'), errors = require('./errorHandling'),
permissions = require('./permissions'),
canThis = permissions.canThis,
ghost = new Ghost(), ghost = new Ghost(),
dataProvider = ghost.dataProvider, dataProvider = ghost.dataProvider,
@ -40,7 +42,15 @@ posts = {
// **takes:** a json object with all the properties which should be updated // **takes:** a json object with all the properties which should be updated
edit: function edit(postData) { edit: function edit(postData) {
// **returns:** a promise for the resulting post in a json object // **returns:** a promise for the resulting post in a json object
return dataProvider.Post.edit(postData); if (!this.user) {
return when.reject("You do not have permission to edit this post.");
}
return canThis(this.user).edit.post(postData.id).then(function () {
return dataProvider.Post.edit(postData);
}, function () {
return when.reject("You do not have permission to edit this post.");
});
}, },
// #### Add // #### Add
@ -48,7 +58,15 @@ posts = {
// **takes:** a json object representing a post, // **takes:** a json object representing a post,
add: function add(postData) { add: function add(postData) {
// **returns:** a promise for the resulting post in a json object // **returns:** a promise for the resulting post in a json object
return dataProvider.Post.add(postData); if (!this.user) {
return when.reject("You do not have permission to add posts.");
}
return canThis(this.user).create.post().then(function () {
return dataProvider.Post.add(postData);
}, function () {
return when.reject("You do not have permission to add posts.");
});
}, },
// #### Destroy // #### Destroy
@ -56,7 +74,15 @@ posts = {
// **takes:** an identifier (id or slug?) // **takes:** an identifier (id or slug?)
destroy: function destroy(args) { destroy: function destroy(args) {
// **returns:** a promise for a json response with the id of the deleted post // **returns:** a promise for a json response with the id of the deleted post
return dataProvider.Post.destroy(args.id); if (!this.user) {
return when.reject("You do not have permission to remove posts.");
}
return canThis(this.user).remove.post(args.id).then(function () {
return dataProvider.Post.destroy(args.id);
}, function () {
return when.reject("You do not have permission to remove posts.");
});
} }
}; };

View File

@ -231,20 +231,30 @@ Post = GhostBookshelf.Model.extend({
}, errors.logAndThrowError); }, errors.logAndThrowError);
} }
// TODO: This logic is temporary, will probably need to be updated // Check if any permissions apply for this user and post.
hasPermission = _.any(userPermissions, function (perm) { hasPermission = _.any(userPermissions, function (perm) {
if (perm.get('object_type') !== 'post') { // Check for matching action type and object type
if (perm.get('action_type') !== action_type ||
perm.get('object_type') !== 'post') {
return false; return false;
} }
// True, if no object_id specified, or it matches // If asking whether we can create posts,
// and we have a create posts permission then go ahead and say yes
if (action_type === 'create' && perm.get('action_type') === action_type) {
return true;
}
// Check for either no object id or a matching one
return !perm.get('object_id') || perm.get('object_id') === postModel.id; return !perm.get('object_id') || perm.get('object_id') === postModel.id;
}); });
// If this is the author of the post, allow it. // If this is the author of the post, allow it.
hasPermission = hasPermission || userId === postModel.get('author_id'); // Moved below the permissions checks because there may not be a postModel
// in the case like canThis(user).create.post()
hasPermission = hasPermission || (postModel && userId === postModel.get('author_id'));
// Resolve if we have appropriate permissions
if (hasPermission) { if (hasPermission) {
return when.resolve(); return when.resolve();
} }

View File

@ -54,15 +54,9 @@ User = GhostBookshelf.Model.extend({
*/ */
add: function (_user) { add: function (_user) {
var User = this, var self = this,
// Clone the _user so we don't expose the hashed password unnecessarily // Clone the _user so we don't expose the hashed password unnecessarily
userData = _.extend({}, _user), userData = _.extend({}, _user);
fail = false,
userRoles = {
"role_id": 1,
"user_id": 1
};
/** /**
* This only allows one user to be added to the database, otherwise fails. * This only allows one user to be added to the database, otherwise fails.
@ -70,20 +64,27 @@ User = GhostBookshelf.Model.extend({
* @author javorszky * @author javorszky
*/ */
return this.forge().fetch().then(function (user) { return this.forge().fetch().then(function (user) {
// Check if user exists
if (user) { if (user) {
fail = true;
}
if (fail) {
return when.reject(new Error('A user is already registered. Only one user for now!')); return when.reject(new Error('A user is already registered. Only one user for now!'));
} }
return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) { // Hash the provided password with bcrypt
userData.password = hash; return nodefn.call(bcrypt.hash, _user.password, null, null);
GhostBookshelf.Model.add.call(UserRole, userRoles); }).then(function (hash) {
return GhostBookshelf.Model.add.call(User, userData); // Assign the hashed password
}, errors.logAndThrowError); userData.password = hash;
// Save the user with the hashed password
return GhostBookshelf.Model.add.call(self, userData);
}).then(function (addedUser) {
// Assign the userData to our created user so we can pass it back
userData = addedUser;
// Add this user to the admin role (assumes admin = role_id: 1)
return UserRole.add({role_id: 1, user_id: addedUser.id});
}).then(function (addedUserRole) {
// Return the added user as expected
return when.resolve(userData);
}, errors.logAndThrowError); }, errors.logAndThrowError);
/** /**

View File

@ -13,6 +13,14 @@ var _ = require('underscore'),
CanThisResult, CanThisResult,
exported; exported;
function hasActionsMap() {
// Just need to find one key in the actionsMap
return _.any(exported.actionsMap, function (val, key) {
return Object.hasOwnProperty(key);
});
}
// Base class for canThis call results // Base class for canThis call results
CanThisResult = function () { CanThisResult = function () {
this.userPermissionLoad = false; this.userPermissionLoad = false;
@ -98,14 +106,16 @@ CanThisResult.prototype.beginCheck = function (user) {
var self = this, var self = this,
userId = user.id || user; userId = user.id || user;
if (!hasActionsMap()) {
throw new Error("No actions map found, please call permissions.init() before use.");
}
// 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(userId); 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
// TODO: Throw error if not init() yet?
_.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()

View File

@ -224,7 +224,7 @@ describe('permissions', function () {
// TODO: Verify updatedUser.related('permissions') has the permission? // TODO: Verify updatedUser.related('permissions') has the permission?
var canThisResult = permissions.canThis(updatedUser); var canThisResult = permissions.canThis(updatedUser.id);
should.exist(canThisResult.edit); should.exist(canThisResult.edit);
should.exist(canThisResult.edit.post); should.exist(canThisResult.edit.post);