0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-18 02:21:47 -05:00

Merge pull request from ErisDS/issue-3096

User Permissions: Edit, Add, Destroy & Role management
This commit is contained in:
Sebastian Gierlinger 2014-07-28 12:04:07 +02:00
commit cc471aedcb
14 changed files with 1321 additions and 372 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

@ -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'
}
],

View file

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