mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Merge pull request #3395 from ErisDS/issue-3096
User Permissions: Edit, Add, Destroy & Role management
This commit is contained in:
commit
cc471aedcb
14 changed files with 1321 additions and 372 deletions
core
server
test
integration/api
unit
utils
|
@ -10,7 +10,6 @@ var when = require('when'),
|
|||
globalUtils = require('../utils'),
|
||||
config = require('../config'),
|
||||
mail = require('./mail'),
|
||||
rolesAPI = require('./roles'),
|
||||
|
||||
docName = 'users',
|
||||
ONE_DAY = 60 * 60 * 24 * 1000,
|
||||
|
@ -44,8 +43,8 @@ users = {
|
|||
options.include = prepareInclude(options.include);
|
||||
}
|
||||
return dataProvider.User.findPage(options);
|
||||
}, function () {
|
||||
return when.reject(new errors.NoPermissionError('You do not have permission to browse users.'));
|
||||
}).catch(function (error) {
|
||||
return errors.handleAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -84,48 +83,44 @@ users = {
|
|||
* @returns {Promise(User)}
|
||||
*/
|
||||
edit: function edit(object, options) {
|
||||
var editOperation;
|
||||
if (options.id === 'me' && options.context && options.context.user) {
|
||||
options.id = options.context.user;
|
||||
}
|
||||
|
||||
return canThis(options.context).edit.user(options.id).then(function () {
|
||||
// TODO: add permission check for roles
|
||||
// if (data.roles) {
|
||||
// return canThis(options.context).assign.role(<role-id>)
|
||||
// }
|
||||
// }.then(function (){
|
||||
return utils.checkObject(object, docName).then(function (checkedUserData) {
|
||||
if (options.include) {
|
||||
options.include = prepareInclude(options.include);
|
||||
}
|
||||
|
||||
if (options.include) {
|
||||
options.include = prepareInclude(options.include);
|
||||
return utils.checkObject(object, docName).then(function (data) {
|
||||
// Edit operation
|
||||
editOperation = function () {
|
||||
return dataProvider.User.edit(data.users[0], options)
|
||||
.then(function (result) {
|
||||
if (result) {
|
||||
return { users: [result.toJSON()]};
|
||||
}
|
||||
|
||||
return when.reject(new errors.NotFoundError('User not found.'));
|
||||
});
|
||||
};
|
||||
|
||||
// Check permissions
|
||||
return canThis(options.context).edit.user(options.id).then(function () {
|
||||
if (data.users[0].roles) {
|
||||
if (options.id === options.context.user) {
|
||||
return when.reject(new errors.NoPermissionError('You cannot change your own role.'));
|
||||
}
|
||||
return canThis(options.context).assign.role(data.users[0].roles[0]).then(function () {
|
||||
return editOperation();
|
||||
});
|
||||
}
|
||||
|
||||
return dataProvider.User.edit(checkedUserData.users[0], options);
|
||||
}).then(function (result) {
|
||||
if (result) {
|
||||
return { users: [result.toJSON()]};
|
||||
}
|
||||
return when.reject(new errors.NotFoundError('User not found.'));
|
||||
});
|
||||
}, function () {
|
||||
return when.reject(new errors.NoPermissionError('You do not have permission to edit this user.'));
|
||||
});
|
||||
},
|
||||
return editOperation();
|
||||
|
||||
/**
|
||||
* ### Destroy
|
||||
* @param {{id, context}} options
|
||||
* @returns {Promise(User)}
|
||||
*/
|
||||
destroy: function destroy(options) {
|
||||
return canThis(options.context).destroy.user(options.id).then(function () {
|
||||
return users.read(options).then(function (result) {
|
||||
return dataProvider.User.destroy(options).then(function () {
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}, function () {
|
||||
return when.reject(new errors.NoPermissionError('You do not have permission to remove the user.'));
|
||||
}).catch(function (error) {
|
||||
return errors.handleAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -137,23 +132,26 @@ users = {
|
|||
*/
|
||||
add: function add(object, options) {
|
||||
var newUser,
|
||||
user;
|
||||
user,
|
||||
roleId;
|
||||
|
||||
return canThis(options.context).add.user().then(function () {
|
||||
return canThis(options.context).add.user(object).then(function () {
|
||||
return utils.checkObject(object, docName).then(function (checkedUserData) {
|
||||
if (options.include) {
|
||||
options.include = prepareInclude(options.include);
|
||||
}
|
||||
|
||||
newUser = checkedUserData.users[0];
|
||||
newUser.role = parseInt(newUser.roles[0].id || newUser.roles[0], 10);
|
||||
roleId = parseInt(newUser.roles[0].id || newUser.roles[0], 10);
|
||||
|
||||
return rolesAPI.browse({ context: options.context, permissions: 'assign' }).then(function (results) {
|
||||
// Make sure user is allowed to add a user with this role
|
||||
if (!_.any(results.roles, { id: newUser.role })) {
|
||||
return when.reject(new errors.NoPermissionError('Not allowed to create user with that role.'));
|
||||
// Make sure user is allowed to add a user with this role
|
||||
return dataProvider.Role.findOne({id: roleId}).then(function (role) {
|
||||
if (role.get('name') === 'Owner') {
|
||||
return when.reject(new errors.NoPermissionError('Not allowed to create an owner user.'));
|
||||
}
|
||||
|
||||
return canThis(options.context).assign.role(role);
|
||||
}).then(function () {
|
||||
if (newUser.email) {
|
||||
newUser.name = object.users[0].email.substring(0, newUser.email.indexOf('@'));
|
||||
newUser.password = globalUtils.uid(50);
|
||||
|
@ -161,6 +159,8 @@ users = {
|
|||
} else {
|
||||
return when.reject(new errors.BadRequestError('No email provided.'));
|
||||
}
|
||||
}).catch(function () {
|
||||
return when.reject(new errors.NoPermissionError('Not allowed to create user with that role.'));
|
||||
});
|
||||
}).then(function () {
|
||||
return dataProvider.User.getByEmail(newUser.email);
|
||||
|
@ -208,7 +208,7 @@ users = {
|
|||
});
|
||||
}).then(function () {
|
||||
return when.resolve({users: [user]});
|
||||
}).otherwise(function (error) {
|
||||
}).catch(function (error) {
|
||||
if (error && error.type === 'EmailError') {
|
||||
error.message = 'Error sending email: ' + error.message + ' Please check your email settings and resend the invitation.';
|
||||
errors.logWarn(error.message);
|
||||
|
@ -222,11 +222,30 @@ users = {
|
|||
}
|
||||
return when.reject(error);
|
||||
});
|
||||
}, function () {
|
||||
return when.reject(new errors.NoPermissionError('You do not have permission to add a user.'));
|
||||
}).catch(function (error) {
|
||||
return errors.handleAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* ### Destroy
|
||||
* @param {{id, context}} options
|
||||
* @returns {Promise(User)}
|
||||
*/
|
||||
destroy: function destroy(options) {
|
||||
return canThis(options.context).destroy.user(options.id).then(function () {
|
||||
return users.read(options).then(function (result) {
|
||||
return dataProvider.User.destroy(options).then(function () {
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}).catch(function (error) {
|
||||
return errors.handleAPIError(error);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* ### Change Password
|
||||
* @param {password} object
|
||||
|
@ -244,7 +263,7 @@ users = {
|
|||
|
||||
return dataProvider.User.changePassword(oldPassword, newPassword, ne2Password, options).then(function () {
|
||||
return when.resolve({password: [{message: 'Password changed successfully.'}]});
|
||||
}).otherwise(function (error) {
|
||||
}).catch(function (error) {
|
||||
return when.reject(new errors.ValidationError(error.message));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ utils = {
|
|||
*/
|
||||
checkObject: function (object, docName) {
|
||||
if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) {
|
||||
return when.reject(new errors.BadRequestError('No root key (\'' + docName + '\') provided.'));
|
||||
return errors.logAndRejectError(new errors.BadRequestError('No root key (\'' + docName + '\') provided.'));
|
||||
}
|
||||
|
||||
// convert author property to author_id to match the name in the database
|
||||
|
@ -27,7 +27,7 @@ utils = {
|
|||
delete object.posts[0].author;
|
||||
}
|
||||
}
|
||||
return when.resolve(object);
|
||||
return when(object);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ errors = {
|
|||
|
||||
throwError: function (err) {
|
||||
if (!err) {
|
||||
err = new Error("An error occurred");
|
||||
err = new Error('An error occurred');
|
||||
}
|
||||
|
||||
if (_.isString(err)) {
|
||||
|
@ -137,7 +137,7 @@ errors = {
|
|||
logAndRejectError: function (err, context, help) {
|
||||
this.logError(err, context, help);
|
||||
|
||||
this.rejectError(err, context, help);
|
||||
return this.rejectError(err, context, help);
|
||||
},
|
||||
|
||||
logErrorWithRedirect: function (msg, context, help, redirectTo, req, res) {
|
||||
|
@ -153,6 +153,22 @@ errors = {
|
|||
};
|
||||
},
|
||||
|
||||
handleAPIError: function (error) {
|
||||
if (!error) {
|
||||
return this.rejectError(new this.NoPermissionError('You do not have permission to perform this action'));
|
||||
}
|
||||
|
||||
if (_.isString(error)) {
|
||||
return this.rejectError(new this.NoPermissionError(error));
|
||||
}
|
||||
|
||||
if (error.type) {
|
||||
return this.rejectError(error);
|
||||
}
|
||||
|
||||
return this.rejectError(new this.InternalServerError(error));
|
||||
},
|
||||
|
||||
renderErrorPage: function (code, err, req, res, next) {
|
||||
/*jshint unused:false*/
|
||||
var self = this;
|
||||
|
@ -207,17 +223,18 @@ errors = {
|
|||
|
||||
// And then try to explain things to the user...
|
||||
// Cheat and output the error using handlebars escapeExpression
|
||||
return res.send(500, "<h1>Oops, seems there is an an error in the error template.</h1>"
|
||||
+ "<p>Encountered the error: </p>"
|
||||
+ "<pre>" + hbs.handlebars.Utils.escapeExpression(templateErr.message || templateErr) + "</pre>"
|
||||
+ "<br ><p>whilst trying to render an error page for the error: </p>"
|
||||
+ code + " " + "<pre>" + hbs.handlebars.Utils.escapeExpression(err.message || err) + "</pre>"
|
||||
);
|
||||
return res.send(500,
|
||||
'<h1>Oops, seems there is an an error in the error template.</h1>' +
|
||||
'<p>Encountered the error: </p>' +
|
||||
'<pre>' + hbs.handlebars.Utils.escapeExpression(templateErr.message || templateErr) + '</pre>' +
|
||||
'<br ><p>whilst trying to render an error page for the error: </p>' +
|
||||
code + ' ' + '<pre>' + hbs.handlebars.Utils.escapeExpression(err.message || err) + '</pre>'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (code >= 500) {
|
||||
this.logError(err, "Rendering Error Page", "Ghost caught a processing error in the middleware layer.");
|
||||
this.logError(err, 'Rendering Error Page', 'Ghost caught a processing error in the middleware layer.');
|
||||
}
|
||||
|
||||
// Are we admin? If so, don't worry about the user template
|
||||
|
@ -230,7 +247,7 @@ errors = {
|
|||
},
|
||||
|
||||
error404: function (req, res, next) {
|
||||
var message = res.isAdmin && req.user ? "No Ghost Found" : "Page Not Found";
|
||||
var message = res.isAdmin && req.user ? 'No Ghost Found' : 'Page Not Found';
|
||||
|
||||
// do not cache 404 error
|
||||
res.set({'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'});
|
||||
|
@ -271,8 +288,16 @@ errors = {
|
|||
// Ensure our 'this' context for methods and preserve method arity by
|
||||
// using Function#bind for expressjs
|
||||
_.each([
|
||||
'logWarn',
|
||||
'logInfo',
|
||||
'rejectError',
|
||||
'throwError',
|
||||
'logError',
|
||||
'logAndThrowError',
|
||||
'logAndRejectError',
|
||||
'logErrorAndExit',
|
||||
'logErrorWithRedirect',
|
||||
'handleAPIError',
|
||||
'renderErrorPage',
|
||||
'error404',
|
||||
'error500'
|
||||
|
|
|
@ -521,7 +521,7 @@ Post = ghostBookshelf.Model.extend({
|
|||
});
|
||||
},
|
||||
|
||||
permissable: function (postModelOrId, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
permissible: function (postModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
var self = this,
|
||||
postModel = postModelOrId,
|
||||
origArgs;
|
||||
|
@ -536,7 +536,7 @@ Post = ghostBookshelf.Model.extend({
|
|||
// Build up the original args but substitute with actual model
|
||||
var newArgs = [foundPostModel].concat(origArgs);
|
||||
|
||||
return self.permissable.apply(self, newArgs);
|
||||
return self.permissible.apply(self, newArgs);
|
||||
}, errors.logAndThrowError);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ Role = ghostBookshelf.Model.extend({
|
|||
},
|
||||
|
||||
|
||||
permissable: function (roleModelOrId, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
permissible: function (roleModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
var self = this,
|
||||
checkAgainst = [],
|
||||
origArgs;
|
||||
|
@ -55,23 +55,20 @@ Role = ghostBookshelf.Model.extend({
|
|||
// Build up the original args but substitute with actual model
|
||||
var newArgs = [foundRoleModel].concat(origArgs);
|
||||
|
||||
return self.permissable.apply(self, newArgs);
|
||||
return self.permissible.apply(self, newArgs);
|
||||
}, errors.logAndThrowError);
|
||||
}
|
||||
|
||||
switch (loadedPermissions.user) {
|
||||
case 'Owner':
|
||||
case 'Administrator':
|
||||
if (action === 'assign' && loadedPermissions.user) {
|
||||
if (_.any(loadedPermissions.user.roles, { 'name': 'Owner' }) ||
|
||||
_.any(loadedPermissions.user.roles, { 'name': 'Administrator' })) {
|
||||
checkAgainst = ['Administrator', 'Editor', 'Author'];
|
||||
break;
|
||||
case 'Editor':
|
||||
checkAgainst = ['Editor', 'Author'];
|
||||
}
|
||||
} else if (_.any(loadedPermissions.user.roles, { 'name': 'Editor' })) {
|
||||
checkAgainst = ['Author'];
|
||||
}
|
||||
|
||||
// If we have a role passed into here
|
||||
if (roleModelOrId && !_.contains(checkAgainst, roleModelOrId.get('name'))) {
|
||||
// Role not in the list of permissible roles
|
||||
hasUserPermission = false;
|
||||
// Role in the list of permissible roles
|
||||
hasUserPermission = roleModelOrId && _.contains(checkAgainst, roleModelOrId.get('name'));
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasAppPermission) {
|
||||
|
|
|
@ -93,6 +93,14 @@ User = ghostBookshelf.Model.extend({
|
|||
|
||||
permissions: function () {
|
||||
return this.belongsToMany('Permission');
|
||||
},
|
||||
|
||||
hasRole: function (roleName) {
|
||||
var roles = this.related('roles');
|
||||
|
||||
return roles.some(function (role) {
|
||||
return role.get('name') === roleName;
|
||||
});
|
||||
}
|
||||
|
||||
}, {
|
||||
|
@ -183,7 +191,9 @@ User = ghostBookshelf.Model.extend({
|
|||
if (options.status && options.status !== 'all') {
|
||||
// make sure that status is valid
|
||||
//TODO: need a better way of getting a list of statuses other than hard-coding them...
|
||||
options.status = _.indexOf(['active', 'warn-1', 'warn-2', 'warn-3', 'locked', 'invited'], options.status) !== -1 ? options.status : 'active';
|
||||
options.status = _.indexOf(
|
||||
['active', 'warn-1', 'warn-2', 'warn-3', 'locked', 'invited'],
|
||||
options.status) !== -1 ? options.status : 'active';
|
||||
options.where.status = options.status;
|
||||
}
|
||||
|
||||
|
@ -300,8 +310,6 @@ User = ghostBookshelf.Model.extend({
|
|||
*/
|
||||
edit: function (data, options) {
|
||||
var self = this,
|
||||
adminRole,
|
||||
ownerRole,
|
||||
roleId;
|
||||
|
||||
options = options || {};
|
||||
|
@ -312,11 +320,10 @@ User = ghostBookshelf.Model.extend({
|
|||
if (data.roles) {
|
||||
roleId = parseInt(data.roles[0].id || data.roles[0], 10);
|
||||
|
||||
if (user.id === options.context.user) {
|
||||
return when.reject(new errors.ValidationError('You are not allowed to assign a new role to yourself'));
|
||||
}
|
||||
if (data.roles.length > 1) {
|
||||
return when.reject(new errors.ValidationError('Only one role per user is supported at the moment.'));
|
||||
return when.reject(
|
||||
new errors.ValidationError('Only one role per user is supported at the moment.')
|
||||
);
|
||||
}
|
||||
|
||||
return user.roles().fetch().then(function (roles) {
|
||||
|
@ -325,27 +332,9 @@ User = ghostBookshelf.Model.extend({
|
|||
return user;
|
||||
}
|
||||
return Role.findOne({id: roleId});
|
||||
}).then(function (role) {
|
||||
if (role && role.get('name') === 'Owner') {
|
||||
// Get admin and owner role
|
||||
return Role.findOne({name: 'Administrator'}).then(function (result) {
|
||||
adminRole = result;
|
||||
return Role.findOne({name: 'Owner'});
|
||||
}).then(function (result) {
|
||||
ownerRole = result;
|
||||
return User.findOne({id: options.context.user});
|
||||
}).then(function (contextUser) {
|
||||
// check if user has the owner role
|
||||
var currentRoles = contextUser.toJSON().roles;
|
||||
if (!_.contains(currentRoles, ownerRole.id)) {
|
||||
return when.reject(new errors.ValidationError('Only owners are able to transfer the owner role.'));
|
||||
}
|
||||
// convert owner to admin
|
||||
return contextUser.roles().updatePivot({role_id: adminRole.id});
|
||||
}).then(function () {
|
||||
// assign owner role to a new user
|
||||
return user.roles().updatePivot({role_id: ownerRole.id});
|
||||
});
|
||||
}).then(function (roleToAssign) {
|
||||
if (roleToAssign && roleToAssign.get('name') === 'Owner') {
|
||||
return self.transferOwnership(user, roleToAssign, options.context);
|
||||
} else {
|
||||
// assign all other roles
|
||||
return user.roles().updatePivot({role_id: roleId});
|
||||
|
@ -371,7 +360,18 @@ User = ghostBookshelf.Model.extend({
|
|||
add: function (data, options) {
|
||||
var self = this,
|
||||
// Clone the _user so we don't expose the hashed password unnecessarily
|
||||
userData = this.filterData(data);
|
||||
userData = this.filterData(data),
|
||||
// Get the role we're going to assign to this user, or the author role if there isn't one
|
||||
// TODO: don't reference Author role by ID!
|
||||
roles = data.roles || [1];
|
||||
|
||||
// remove roles from the object
|
||||
delete data.roles;
|
||||
|
||||
// check for too many roles
|
||||
if (roles.length > 1) {
|
||||
return when.reject(new errors.ValidationError('Only one role per user is supported at the moment.'));
|
||||
}
|
||||
|
||||
options = this.filterOptions(options, 'add');
|
||||
options.withRelated = _.union([ 'roles' ], options.include);
|
||||
|
@ -392,13 +392,9 @@ User = ghostBookshelf.Model.extend({
|
|||
}).then(function (addedUser) {
|
||||
// Assign the userData to our created user so we can pass it back
|
||||
userData = addedUser;
|
||||
if (!data.role) {
|
||||
// TODO: needs change when owner role is introduced and setup is changed
|
||||
data.role = 1;
|
||||
}
|
||||
return userData.roles().attach(data.role);
|
||||
}).then(function (addedUserRole) {
|
||||
/*jshint unused:false*/
|
||||
|
||||
return userData.roles().attach(roles);
|
||||
}).then(function () {
|
||||
// find and return the added user
|
||||
return self.findOne({id: userData.id}, options);
|
||||
});
|
||||
|
@ -430,14 +426,14 @@ User = ghostBookshelf.Model.extend({
|
|||
});
|
||||
},
|
||||
|
||||
permissable: function (userModelOrId, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
permissible: function (userModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
var self = this,
|
||||
userModel = userModelOrId,
|
||||
origArgs;
|
||||
|
||||
// If we passed in an id instead of a model, get the model
|
||||
// then check the permissions
|
||||
// If we passed in an id instead of a model, get the model then check the permissions
|
||||
if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
|
||||
|
||||
// Grab the original args without the first one
|
||||
origArgs = _.toArray(arguments).slice(1);
|
||||
// Get the actual post model
|
||||
|
@ -445,13 +441,41 @@ User = ghostBookshelf.Model.extend({
|
|||
// Build up the original args but substitute with actual model
|
||||
var newArgs = [foundUserModel].concat(origArgs);
|
||||
|
||||
return self.permissable.apply(self, newArgs);
|
||||
return self.permissible.apply(self, newArgs);
|
||||
}, errors.logAndThrowError);
|
||||
}
|
||||
|
||||
if (userModel) {
|
||||
// If this is the same user that requests the operation allow it.
|
||||
hasUserPermission = hasUserPermission || context.user === userModel.get('id');
|
||||
if (action === 'edit') {
|
||||
// Users with the role 'Editor' and 'Author' have complex permissions when the action === 'edit'
|
||||
// We now have all the info we need to construct the permissions
|
||||
if (_.any(loadedPermissions.user.roles, { 'name': 'Author' })) {
|
||||
// If this is the same user that requests the operation allow it.
|
||||
hasUserPermission = hasUserPermission || context.user === userModel.get('id');
|
||||
}
|
||||
|
||||
if (_.any(loadedPermissions.user.roles, { 'name': 'Editor' })) {
|
||||
// If this is the same user that requests the operation allow it.
|
||||
hasUserPermission = context.user === userModel.get('id');
|
||||
|
||||
// Alternatively, if the user we are trying to edit is an Author, allow it
|
||||
hasUserPermission = hasUserPermission || userModel.hasRole('Author');
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'destroy') {
|
||||
// Owner cannot be deleted EVER
|
||||
if (userModel.hasRole('Owner')) {
|
||||
return when.reject();
|
||||
}
|
||||
|
||||
// Users with the role 'Editor' have complex permissions when the action === 'destroy'
|
||||
if (_.any(loadedPermissions.user.roles, { 'name': 'Editor' })) {
|
||||
// If this is the same user that requests the operation allow it.
|
||||
hasUserPermission = context.user === userModel.get('id');
|
||||
|
||||
// Alternatively, if the user we are trying to edit is an Author, allow it
|
||||
hasUserPermission = hasUserPermission || userModel.hasRole('Author');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasAppPermission) {
|
||||
|
@ -490,8 +514,9 @@ User = ghostBookshelf.Model.extend({
|
|||
if (!user) {
|
||||
return when.reject(new errors.NotFoundError('There is no user with that email address.'));
|
||||
}
|
||||
if (user.get('status') === 'invited' || user.get('status') === 'invited-pending'
|
||||
|| user.get('status') === 'inactive') {
|
||||
if (user.get('status') === 'invited' || user.get('status') === 'invited-pending' ||
|
||||
user.get('status') === 'inactive'
|
||||
) {
|
||||
return when.reject(new Error('The user with that email address is inactive.'));
|
||||
}
|
||||
if (user.get('status') !== 'locked') {
|
||||
|
@ -588,24 +613,25 @@ User = ghostBookshelf.Model.extend({
|
|||
|
||||
// Check if invalid structure
|
||||
if (!parts || parts.length !== 3) {
|
||||
return when.reject(new Error("Invalid token structure"));
|
||||
return when.reject(new Error('Invalid token structure'));
|
||||
}
|
||||
|
||||
expires = parseInt(parts[0], 10);
|
||||
email = parts[1];
|
||||
|
||||
if (isNaN(expires)) {
|
||||
return when.reject(new Error("Invalid token expiration"));
|
||||
return when.reject(new Error('Invalid token expiration'));
|
||||
}
|
||||
|
||||
// Check if token is expired to prevent replay attacks
|
||||
if (expires < Date.now()) {
|
||||
return when.reject(new Error("Expired token"));
|
||||
return when.reject(new Error('Expired token'));
|
||||
}
|
||||
|
||||
// to prevent brute force attempts to reset the password the combination of email+expires is only allowed for 10 attempts
|
||||
// to prevent brute force attempts to reset the password the combination of email+expires is only allowed for
|
||||
// 10 attempts
|
||||
if (tokenSecurity[email + '+' + expires] && tokenSecurity[email + '+' + expires].count >= 10) {
|
||||
return when.reject(new Error("Token locked"));
|
||||
return when.reject(new Error('Token locked'));
|
||||
}
|
||||
|
||||
return this.generateResetToken(email, expires, dbHash).then(function (generatedToken) {
|
||||
|
@ -627,8 +653,10 @@ User = ghostBookshelf.Model.extend({
|
|||
}
|
||||
|
||||
// increase the count for email+expires for each failed attempt
|
||||
tokenSecurity[email + '+' + expires] = {count: tokenSecurity[email + '+' + expires] ? tokenSecurity[email + '+' + expires].count + 1 : 1};
|
||||
return when.reject(new Error("Invalid token"));
|
||||
tokenSecurity[email + '+' + expires] = {
|
||||
count: tokenSecurity[email + '+' + expires] ? tokenSecurity[email + '+' + expires].count + 1 : 1
|
||||
};
|
||||
return when.reject(new Error('Invalid token'));
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -636,7 +664,7 @@ User = ghostBookshelf.Model.extend({
|
|||
var self = this;
|
||||
|
||||
if (newPassword !== ne2Password) {
|
||||
return when.reject(new Error("Your new passwords do not match"));
|
||||
return when.reject(new Error('Your new passwords do not match'));
|
||||
}
|
||||
|
||||
return validatePasswordLength(newPassword).then(function () {
|
||||
|
@ -657,10 +685,30 @@ User = ghostBookshelf.Model.extend({
|
|||
});
|
||||
},
|
||||
|
||||
transferOwnership: function (user, ownerRole, context) {
|
||||
var adminRole;
|
||||
// Get admin role
|
||||
return Role.findOne({name: 'Administrator'}).then(function (result) {
|
||||
adminRole = result;
|
||||
return User.findOne({id: context.user});
|
||||
}).then(function (contextUser) {
|
||||
// check if user has the owner role
|
||||
var currentRoles = contextUser.toJSON().roles;
|
||||
if (!_.contains(currentRoles, ownerRole.id)) {
|
||||
return when.reject(new errors.NoPermissionError('Only owners are able to transfer the owner role.'));
|
||||
}
|
||||
// convert owner to admin
|
||||
return contextUser.roles().updatePivot({role_id: adminRole.id});
|
||||
}).then(function () {
|
||||
// assign owner role to a new user
|
||||
return user.roles().updatePivot({role_id: ownerRole.id});
|
||||
});
|
||||
},
|
||||
|
||||
gravatarLookup: function (userData) {
|
||||
var gravatarUrl = '//www.gravatar.com/avatar/' +
|
||||
crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') +
|
||||
"?d=404&s=250",
|
||||
'?d=404&s=250',
|
||||
checkPromise = when.defer();
|
||||
|
||||
http.get('http:' + gravatarUrl, function (res) {
|
||||
|
@ -675,7 +723,6 @@ User = ghostBookshelf.Model.extend({
|
|||
|
||||
return checkPromise.promise;
|
||||
},
|
||||
|
||||
// Get the user by email address, enforces case insensitivity rejects if the user is not found
|
||||
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
|
||||
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
|
||||
|
|
|
@ -13,11 +13,6 @@ var effective = {
|
|||
allPerms = [],
|
||||
user = foundUser.toJSON();
|
||||
|
||||
// TODO: using 'Owner' as return value is a bit hacky.
|
||||
if (_.find(user.roles, { 'name': 'Owner' })) {
|
||||
return 'Owner';
|
||||
}
|
||||
|
||||
rolePerms.push(foundUser.related('permissions').models);
|
||||
|
||||
_.each(rolePerms, function (rolePermGroup) {
|
||||
|
@ -34,7 +29,7 @@ var effective = {
|
|||
});
|
||||
});
|
||||
|
||||
return allPerms;
|
||||
return {permissions: allPerms, roles: user.roles};
|
||||
}, errors.logAndThrowError);
|
||||
},
|
||||
|
||||
|
@ -45,7 +40,7 @@ var effective = {
|
|||
return [];
|
||||
}
|
||||
|
||||
return foundApp.related('permissions').models;
|
||||
return {permissions: foundApp.related('permissions').models};
|
||||
}, errors.logAndThrowError);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -78,8 +78,8 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
|
|||
// Wait for the user loading to finish
|
||||
return permissionLoad.then(function (loadedPermissions) {
|
||||
// Iterate through the user permissions looking for an affirmation
|
||||
var userPermissions = loadedPermissions.user,
|
||||
appPermissions = loadedPermissions.app,
|
||||
var userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null,
|
||||
appPermissions = loadedPermissions.app ? loadedPermissions.app.permissions : null,
|
||||
hasUserPermission,
|
||||
hasAppPermission,
|
||||
checkPermission = function (perm) {
|
||||
|
@ -105,15 +105,14 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
|
|||
return modelId === permObjId;
|
||||
};
|
||||
// Check user permissions for matching action, object and id.
|
||||
if (!_.isEmpty(userPermissions)) {
|
||||
// TODO: using 'Owner' is a bit hacky.
|
||||
if (userPermissions === 'Owner') {
|
||||
hasUserPermission = true;
|
||||
} else {
|
||||
hasUserPermission = _.any(userPermissions, checkPermission);
|
||||
}
|
||||
|
||||
if (_.any(loadedPermissions.user.roles, { 'name': 'Owner' })) {
|
||||
hasUserPermission = true;
|
||||
} else if (!_.isEmpty(userPermissions)) {
|
||||
hasUserPermission = _.any(userPermissions, checkPermission);
|
||||
}
|
||||
|
||||
|
||||
// Check app permissions if they were passed
|
||||
hasAppPermission = true;
|
||||
if (!_.isNull(appPermissions)) {
|
||||
|
@ -121,8 +120,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
|
|||
}
|
||||
|
||||
// Offer a chance for the TargetModel to override the results
|
||||
if (TargetModel && _.isFunction(TargetModel.permissable)) {
|
||||
return TargetModel.permissable(modelId, context, loadedPermissions, hasUserPermission, hasAppPermission);
|
||||
if (TargetModel && _.isFunction(TargetModel.permissible)) {
|
||||
return TargetModel.permissible(
|
||||
modelId, act_type, context, loadedPermissions, hasUserPermission, hasAppPermission
|
||||
);
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasAppPermission) {
|
||||
|
|
124
core/test/integration/api/api_roles_spec.js
Normal file
124
core/test/integration/api/api_roles_spec.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
/*jshint expr:true*/
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
|
||||
// Stuff we are testing
|
||||
RoleAPI = require('../../../server/api/roles'),
|
||||
context = testUtils.context;
|
||||
|
||||
describe('Roles API', function () {
|
||||
// Keep the DB clean
|
||||
before(testUtils.teardown);
|
||||
afterEach(testUtils.teardown);
|
||||
beforeEach(testUtils.setup('users:roles', 'perms:role', 'perms:init'));
|
||||
|
||||
describe('Browse', function () {
|
||||
function checkBrowseResponse(response) {
|
||||
should.exist(response);
|
||||
testUtils.API.checkResponse(response, 'roles');
|
||||
should.exist(response.roles);
|
||||
response.roles.should.have.length(4);
|
||||
testUtils.API.checkResponse(response.roles[0], 'role');
|
||||
testUtils.API.checkResponse(response.roles[1], 'role');
|
||||
testUtils.API.checkResponse(response.roles[2], 'role');
|
||||
testUtils.API.checkResponse(response.roles[3], 'role');
|
||||
}
|
||||
|
||||
it('Owner can browse', function (done) {
|
||||
RoleAPI.browse(context.owner).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Admin can browse', function (done) {
|
||||
RoleAPI.browse(context.admin).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Editor can browse', function (done) {
|
||||
RoleAPI.browse(context.editor).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Author can browse', function (done) {
|
||||
RoleAPI.browse(context.author).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('No-auth CANNOT browse', function (done) {
|
||||
RoleAPI.browse().then(function () {
|
||||
done(new Error('Browse roles is not denied without authentication.'));
|
||||
}, function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Browse permissions=assign', function () {
|
||||
function checkBrowseResponse(response) {
|
||||
should.exist(response);
|
||||
should.exist(response.roles);
|
||||
testUtils.API.checkResponse(response, 'roles');
|
||||
response.roles.should.have.length(3);
|
||||
testUtils.API.checkResponse(response.roles[0], 'role');
|
||||
testUtils.API.checkResponse(response.roles[1], 'role');
|
||||
testUtils.API.checkResponse(response.roles[2], 'role');
|
||||
response.roles[0].name.should.equal('Administrator');
|
||||
response.roles[1].name.should.equal('Editor');
|
||||
response.roles[2].name.should.equal('Author');
|
||||
}
|
||||
|
||||
it('Owner can assign all', function (done) {
|
||||
RoleAPI.browse(_.extend(context.owner, {permissions: 'assign'})).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Admin can assign all', function (done) {
|
||||
RoleAPI.browse(_.extend(context.admin, {permissions: 'assign'})).then(function (response) {
|
||||
checkBrowseResponse(response);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Editor can assign Author', function (done) {
|
||||
RoleAPI.browse(_.extend(context.editor, {permissions: 'assign'})).then(function (response) {
|
||||
should.exist(response);
|
||||
should.exist(response.roles);
|
||||
testUtils.API.checkResponse(response, 'roles');
|
||||
response.roles.should.have.length(1);
|
||||
testUtils.API.checkResponse(response.roles[0], 'role');
|
||||
response.roles[0].name.should.equal('Author');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Author CANNOT assign any', function (done) {
|
||||
RoleAPI.browse(_.extend(context.author, {permissions: 'assign'})).then(function (response) {
|
||||
should.exist(response);
|
||||
should.exist(response.roles);
|
||||
testUtils.API.checkResponse(response, 'roles');
|
||||
response.roles.should.have.length(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('No-auth CANNOT browse', function (done) {
|
||||
RoleAPI.browse({permissions: 'assign'}).then(function () {
|
||||
done(new Error('Browse roles is not denied without authentication.'));
|
||||
}, function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -106,9 +106,9 @@ describe('Permissions', function () {
|
|||
// }).catch(done);
|
||||
// });
|
||||
//
|
||||
// it('can use permissable function on Model to allow something', function (done) {
|
||||
// it('can use permissible function on Model to allow something', function (done) {
|
||||
// var testUser,
|
||||
// permissableStub = sandbox.stub(Models.Post, 'permissable', function () {
|
||||
// permissibleStub = sandbox.stub(Models.Post, 'permissible', function () {
|
||||
// return when.resolve();
|
||||
// });
|
||||
//
|
||||
|
@ -122,22 +122,22 @@ describe('Permissions', function () {
|
|||
// return permissions.canThis({user: testUser.id}).edit.post(123);
|
||||
// })
|
||||
// .then(function () {
|
||||
// permissableStub.restore();
|
||||
// permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false })
|
||||
// permissibleStub.restore();
|
||||
// permissibleStub.calledWith(123, { user: testUser.id, app: null, internal: false })
|
||||
// .should.equal(true);
|
||||
//
|
||||
// done();
|
||||
// })
|
||||
// .catch(function () {
|
||||
// permissableStub.restore();
|
||||
// permissibleStub.restore();
|
||||
//
|
||||
// done(new Error('did not allow testUser'));
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('can use permissable function on Model to forbid something', function (done) {
|
||||
// it('can use permissible function on Model to forbid something', function (done) {
|
||||
// var testUser,
|
||||
// permissableStub = sandbox.stub(Models.Post, 'permissable', function () {
|
||||
// permissibleStub = sandbox.stub(Models.Post, 'permissible', function () {
|
||||
// return when.reject();
|
||||
// });
|
||||
//
|
||||
|
@ -152,13 +152,13 @@ describe('Permissions', function () {
|
|||
// })
|
||||
// .then(function () {
|
||||
//
|
||||
// permissableStub.restore();
|
||||
// permissibleStub.restore();
|
||||
// done(new Error('Allowed testUser to edit post'));
|
||||
// })
|
||||
// .catch(function () {
|
||||
// permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false })
|
||||
// permissibleStub.calledWith(123, { user: testUser.id, app: null, internal: false })
|
||||
// .should.equal(true);
|
||||
// permissableStub.restore();
|
||||
// permissibleStub.restore();
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
|
@ -257,7 +257,7 @@ describe('Permissions', function () {
|
|||
// });
|
||||
//
|
||||
// it('allows \'internal\' to be passed for internal requests', function (done) {
|
||||
// // Using tag here because post implements the custom permissable interface
|
||||
// // Using tag here because post implements the custom permissible interface
|
||||
// permissions.canThis('internal')
|
||||
// .edit
|
||||
// .tag(1)
|
||||
|
@ -270,7 +270,7 @@ describe('Permissions', function () {
|
|||
// });
|
||||
//
|
||||
// it('allows { internal: true } to be passed for internal requests', function (done) {
|
||||
// // Using tag here because post implements the custom permissable interface
|
||||
// // Using tag here because post implements the custom permissible interface
|
||||
// permissions.canThis({ internal: true })
|
||||
// .edit
|
||||
// .tag(1)
|
||||
|
|
|
@ -7,6 +7,7 @@ var url = require('url'),
|
|||
expectedProperties = {
|
||||
posts: ['posts', 'meta'],
|
||||
users: ['users', 'meta'],
|
||||
roles: ['roles'],
|
||||
pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'],
|
||||
post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
|
||||
'featured', 'image', 'status', 'language', 'created_at', 'created_by', 'updated_at',
|
||||
|
@ -45,7 +46,6 @@ function getAdminURL() {
|
|||
|
||||
// make sure the API only returns expected properties only
|
||||
function checkResponseValue(jsonResponse, properties) {
|
||||
Object.keys(jsonResponse).length.should.eql(properties.length);
|
||||
for (var i = 0; i < properties.length; i = i + 1) {
|
||||
// For some reason, settings response objects do not have the 'hasOwnProperty' method
|
||||
if (Object.prototype.hasOwnProperty.call(jsonResponse, properties[i])) {
|
||||
|
@ -53,6 +53,7 @@ function checkResponseValue(jsonResponse, properties) {
|
|||
}
|
||||
jsonResponse.should.have.property(properties[i]);
|
||||
}
|
||||
Object.keys(jsonResponse).length.should.eql(properties.length);
|
||||
}
|
||||
|
||||
function checkResponse(jsonResponse, objectType, additionalProperties) {
|
||||
|
|
|
@ -158,20 +158,20 @@ DataGenerator.Content = {
|
|||
|
||||
roles: [
|
||||
{
|
||||
"name": "Administrator",
|
||||
"description": "Administrators"
|
||||
name: 'Administrator',
|
||||
description: 'Administrators'
|
||||
},
|
||||
{
|
||||
"name": "Editor",
|
||||
"description": "Editors"
|
||||
name: 'Editor',
|
||||
description: 'Editors'
|
||||
},
|
||||
{
|
||||
"name": "Author",
|
||||
"description": "Authors"
|
||||
name: 'Author',
|
||||
description: 'Authors'
|
||||
},
|
||||
{
|
||||
"name": "Owner",
|
||||
"description": "Blog Owner"
|
||||
name: 'Owner',
|
||||
description: 'Blog Owner'
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
@ -141,6 +141,27 @@ fixtures = {
|
|||
});
|
||||
},
|
||||
|
||||
createExtraUsers: function createExtraUsers() {
|
||||
var knex = config.database.knex,
|
||||
// grab 3 more users
|
||||
extraUsers = DataGenerator.Content.users.slice(2, 5);
|
||||
|
||||
extraUsers = _.map(extraUsers, function (user) {
|
||||
return DataGenerator.forKnex.createUser(_.extend({}, user, {
|
||||
email: 'a' + user.email,
|
||||
slug: 'a' + user.slug
|
||||
}));
|
||||
});
|
||||
|
||||
return knex('users').insert(extraUsers).then(function () {
|
||||
return knex('roles_users').insert([
|
||||
{ user_id: 5, role_id: 1},
|
||||
{ user_id: 6, role_id: 2},
|
||||
{ user_id: 7, role_id: 3}
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
insertOne: function insertOne(obj, fn) {
|
||||
var knex = config.database.knex;
|
||||
return knex(obj)
|
||||
|
@ -164,7 +185,7 @@ fixtures = {
|
|||
try {
|
||||
data = JSON.parse(fileContents);
|
||||
} catch (e) {
|
||||
return when.reject(new Error("Failed to parse the file"));
|
||||
return when.reject(new Error('Failed to parse the file'));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -244,6 +265,7 @@ toDoList = {
|
|||
return settings.populateDefaults().then(function () { return SettingsAPI.updateSettingsCache(); });
|
||||
},
|
||||
'users:roles': function createUsersWithRoles() { return fixtures.createUsersWithRoles(); },
|
||||
'users': function createExtraUsers() { return fixtures.createExtraUsers(); },
|
||||
'owner': function insertOwnerUser() { return fixtures.insertOwnerUser(); },
|
||||
'owner:pre': function initOwnerUser() { return fixtures.initOwnerUser(); },
|
||||
'owner:post': function overrideOwnerUser() { return fixtures.overrideOwnerUser(); },
|
||||
|
@ -376,11 +398,31 @@ module.exports = {
|
|||
|
||||
fork: fork,
|
||||
|
||||
// Helpers to make it easier to write tests which are easy to read
|
||||
context: {
|
||||
internal: {context: {internal: true}},
|
||||
owner: {context: {user: 1}},
|
||||
admin: {context: {user: 2}},
|
||||
editor: {context: {user: 3}},
|
||||
author: {context: {user: 4}}
|
||||
},
|
||||
users: {
|
||||
ids: {
|
||||
owner: 1,
|
||||
admin: 2,
|
||||
editor: 3,
|
||||
author: 4,
|
||||
admin2: 5,
|
||||
editor2: 6,
|
||||
author2: 7
|
||||
}
|
||||
},
|
||||
roles: {
|
||||
ids: {
|
||||
owner: 4,
|
||||
admin: 1,
|
||||
editor: 2,
|
||||
author: 3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue