0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Add permissions to API

closes #2264
- added permissions check to db, users and posts
- added register method to users
- added doesUserExist method to users
- added user from session to internal calls
- changed permissible to overwrite canThis
- removed action map and action type from permissable method
This commit is contained in:
Sebastian Gierlinger 2014-04-08 15:40:33 +02:00
parent 61e94a6e8b
commit e47e9c62d0
15 changed files with 259 additions and 180 deletions

View file

@ -7,6 +7,7 @@ var dataExport = require('../data/export'),
_ = require('lodash'),
validation = require('../data/validation'),
errors = require('../../server/errorHandling'),
canThis = require('../permissions').canThis,
api = {},
db;
@ -15,85 +16,102 @@ api.settings = require('./settings');
db = {
'exportContent': function () {
var self = this;
// Export data, otherwise send error 500
return dataExport().otherwise(function (error) {
return when.reject({errorCode: 500, message: error.message || error});
return canThis(self.user).exportContent.db().then(function () {
return dataExport().otherwise(function (error) {
return when.reject({errorCode: 500, message: error.message || error});
});
}, function () {
return when.reject({code: 403, message: 'You do not have permission to export data. (no rights)'});
});
},
'importContent': function (options) {
var databaseVersion;
var databaseVersion,
self = this;
if (!options.importfile || !options.importfile.path || options.importfile.name.indexOf('json') === -1) {
/**
* Notify of an error if it occurs
*
* - If there's no file (although if you don't select anything, the input is still submitted, so
* !req.files.importfile will always be false)
* - If there is no path
* - If the name doesn't have json in it
*/
return when.reject({code: 500, message: 'Please select a .json file to import.'});
}
return api.settings.read({ key: 'databaseVersion' }).then(function (setting) {
return when(setting.value);
}, function () {
return when('002');
}).then(function (version) {
databaseVersion = version;
// Read the file contents
return nodefn.call(fs.readFile, options.importfile.path);
}).then(function (fileContents) {
var importData,
error = '';
// Parse the json data
try {
importData = JSON.parse(fileContents);
} catch (e) {
errors.logError(e, "API DB import content", "check that the import file is valid JSON.");
return when.reject(new Error("Failed to parse the import JSON file"));
return canThis(self.user).importContent.db().then(function () {
if (!options.importfile || !options.importfile.path || options.importfile.name.indexOf('json') === -1) {
/**
* Notify of an error if it occurs
*
* - If there's no file (although if you don't select anything, the input is still submitted, so
* !req.files.importfile will always be false)
* - If there is no path
* - If the name doesn't have json in it
*/
return when.reject({code: 500, message: 'Please select a .json file to import.'});
}
if (!importData.meta || !importData.meta.version) {
return when.reject(new Error("Import data does not specify version"));
}
return api.settings.read({ key: 'databaseVersion' }).then(function (setting) {
return when(setting.value);
}, function () {
return when('002');
}).then(function (version) {
databaseVersion = version;
// Read the file contents
return nodefn.call(fs.readFile, options.importfile.path);
}).then(function (fileContents) {
var importData,
error = '';
_.each(_.keys(importData.data), function (tableName) {
_.each(importData.data[tableName], function (importValues) {
try {
validation.validateSchema(tableName, importValues);
} catch (err) {
error += error !== "" ? "<br>" : "";
error += err.message;
}
// Parse the json data
try {
importData = JSON.parse(fileContents);
} catch (e) {
errors.logError(e, "API DB import content", "check that the import file is valid JSON.");
return when.reject(new Error("Failed to parse the import JSON file"));
}
if (!importData.meta || !importData.meta.version) {
return when.reject(new Error("Import data does not specify version"));
}
_.each(_.keys(importData.data), function (tableName) {
_.each(importData.data[tableName], function (importValues) {
try {
validation.validateSchema(tableName, importValues);
} catch (err) {
error += error !== "" ? "<br>" : "";
error += err.message;
}
});
});
if (error !== "") {
return when.reject(new Error(error));
}
// Import for the current version
return dataImport(databaseVersion, importData);
}).then(function importSuccess() {
return api.settings.updateSettingsCache();
}).then(function () {
return when.resolve({message: 'Posts, tags and other data successfully imported'});
}).otherwise(function importFailure(error) {
return when.reject({code: 500, message: error.message || error});
}).finally(function () {
// Unlink the file after import
return nodefn.call(fs.unlink, options.importfile.path);
});
if (error !== "") {
return when.reject(new Error(error));
}
// Import for the current version
return dataImport(databaseVersion, importData);
}).then(function importSuccess() {
return api.settings.updateSettingsCache();
}).then(function () {
return when.resolve({message: 'Posts, tags and other data successfully imported'});
}).otherwise(function importFailure(error) {
return when.reject({code: 500, message: error.message || error});
}).finally(function () {
// Unlink the file after import
return nodefn.call(fs.unlink, options.importfile.path);
}, function () {
return when.reject({code: 403, message: 'You do not have permission to export data. (no rights)'});
});
},
'deleteAllContent': function () {
return when(dataProvider.deleteAllContent())
.then(function () {
return when.resolve({message: 'Successfully deleted all content from your blog.'});
}, function (error) {
return when.reject({code: 500, message: error.message || error});
});
var self = this;
return canThis(self.user).deleteAllContent.db().then(function () {
return when(dataProvider.deleteAllContent())
.then(function () {
return when.resolve({message: 'Successfully deleted all content from your blog.'});
}, function (error) {
return when.reject({code: 500, message: error.message || error});
});
}, function () {
return when.reject({code: 403, message: 'You do not have permission to export data. (no rights)'});
});
}
};

View file

@ -51,13 +51,13 @@ requestHandler = function (apiMethod) {
};
return apiMethod.call(apiContext, options).then(function (result) {
res.json(result || {});
return cacheInvalidationHeader(req, result).then(function (header) {
if (header) {
res.set({
"X-Cache-Invalidate": header
});
}
res.json(result || {});
});
}, function (error) {
var errorCode = error.code || 500,

View file

@ -20,6 +20,9 @@ posts = {
browse: function browse(options) {
options = options || {};
if (!this.user) {
options.status = 'published';
}
// **returns:** a promise for a page of posts in a json object
return dataProvider.Post.findPage(options).then(function (result) {
var i = 0,
@ -34,9 +37,15 @@ posts = {
// #### Read
// **takes:** an identifier (id or slug?)
read: function read(args) {
read: function read(options) {
options = options || {};
if (!this.user) {
// only published posts for
options.status = 'published';
}
// **returns:** a promise for a single post in a json object
return dataProvider.Post.findOne(args).then(function (result) {
return dataProvider.Post.findOne(options).then(function (result) {
var omitted;
if (result) {
@ -49,15 +58,6 @@ posts = {
});
},
generateSlug: function getSlug(args) {
return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) {
if (slug) {
return slug;
}
return when.reject({code: 500, message: 'Could not generate slug'});
});
},
// #### Edit
// **takes:** a json object with all the properties which should be updated
edit: function edit(postData) {
@ -101,9 +101,11 @@ posts = {
// #### Destroy
// **takes:** an identifier (id or slug?)
destroy: function destroy(args) {
var self = this;
// **returns:** a promise for a json response with the id of the deleted post
return canThis(this.user).remove.post(args.id).then(function () {
return posts.read({id : args.id, status: 'all'}).then(function (result) {
// TODO: Would it be good to get rid of .call()?
return posts.read.call({user: self.user}, {id : args.id, status: 'all'}).then(function (result) {
return dataProvider.Post.destroy(args.id).then(function () {
var deletedObj = result;
return deletedObj;
@ -112,7 +114,25 @@ posts = {
}, function () {
return when.reject({code: 403, message: 'You do not have permission to remove posts.'});
});
},
// #### Generate slug
// **takes:** a string to generate the slug from
generateSlug: function generateSlug(args) {
return canThis(this.user).slug.post().then(function () {
return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) {
if (slug) {
return slug;
}
return when.reject({code: 500, message: 'Could not generate slug'});
});
}, function () {
return when.reject({code: 403, message: 'You do not have permission.'});
});
}
};
module.exports = posts;

View file

@ -2,6 +2,7 @@ var when = require('when'),
_ = require('lodash'),
dataProvider = require('../models'),
settings = require('./settings'),
canThis = require('../permissions').canThis,
ONE_DAY = 86400000,
filteredAttributes = ['password', 'created_by', 'updated_by', 'last_login'],
users;
@ -13,20 +14,23 @@ users = {
// **takes:** options object
browse: function browse(options) {
// **returns:** a promise for a collection of users in a json object
return canThis(this.user).browse.user().then(function () {
return dataProvider.User.browse(options).then(function (result) {
var i = 0,
omitted = {};
return dataProvider.User.browse(options).then(function (result) {
var i = 0,
omitted = {};
if (result) {
omitted = result.toJSON();
}
if (result) {
omitted = result.toJSON();
}
for (i = 0; i < omitted.length; i = i + 1) {
omitted[i] = _.omit(omitted[i], filteredAttributes);
}
for (i = 0; i < omitted.length; i = i + 1) {
omitted[i] = _.omit(omitted[i], filteredAttributes);
}
return omitted;
return omitted;
});
}, function () {
return when.reject({code: 403, message: 'You do not have permission to browse users.'});
});
},
@ -52,22 +56,36 @@ users = {
// **takes:** a json object representing a user
edit: function edit(userData) {
// **returns:** a promise for the resulting user in a json object
var self = this;
userData.id = this.user;
return dataProvider.User.edit(userData, {user: this.user}).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredAttributes);
return omitted;
}
return when.reject({code: 404, message: 'User not found'});
return canThis(this.user).edit.user(userData.id).then(function () {
return dataProvider.User.edit(userData, {user: self.user}).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredAttributes);
return omitted;
}
return when.reject({code: 404, message: 'User not found'});
});
}, function () {
return when.reject({code: 403, message: 'You do not have permission to edit this users.'});
});
},
// #### Add
// **takes:** a json object representing a user
add: function add(userData) {
// **returns:** a promise for the resulting user in a json object
return dataProvider.User.add(userData, {user: this.user});
var self = this;
return canThis(this.user).add.user().then(function () {
// if the user is created by users.register(), use id: 1
// as the creator for now
if (self.user === 'internal') {
self.user = 1;
}
return dataProvider.User.add(userData, {user: self.user});
}, function () {
return when.reject({code: 403, message: 'You do not have permission to add a users.'});
});
},
// #### Register
@ -75,7 +93,7 @@ users = {
register: function register(userData) {
// TODO: if we want to prevent users from being created with the signup form
// this is the right place to do it
return users.add.call({user: 1}, userData);
return users.add.call({user: 'internal'}, userData);
},
// #### Check
@ -111,6 +129,15 @@ users = {
return settings.read('dbHash').then(function (dbHash) {
return dataProvider.User.resetPassword(token, newPassword, ne2Password, dbHash);
});
},
doesUserExist: function doesUserExist() {
return dataProvider.User.browse().then(function (users) {
if (users.length === 0) {
return false;
}
return true;
});
}
};

View file

@ -122,7 +122,7 @@ adminControllers = {
}).otherwise(function (err) {
var notification = {
type: 'error',
message: 'Your export file could not be generated.',
message: 'Your export file could not be generated. Error: ' + err.message,
status: 'persistent',
id: 'errorexport'
};

View file

@ -266,7 +266,7 @@ frontendControllers = {
// TODO: needs refactor for multi user to not use first user as default
return when.settle([
api.users.read({id : 1}),
api.users.read.call({user : 'internal'}, {id : 1}),
api.settings.read('title'),
api.settings.read('description'),
api.settings.read('permalinks')

View file

@ -39,7 +39,7 @@ function ghostLocals(req, res, next) {
if (res.isAdmin) {
res.locals.csrfToken = req.csrfToken();
when.all([
api.users.read({id: req.session.user}),
api.users.read.call({user: req.session.user}, {id: req.session.user}),
api.notifications.browse()
]).then(function (values) {
var currentUser = values[0],
@ -159,8 +159,9 @@ function manageAdminAndTheme(req, res, next) {
// Redirect to signup if no users are currently created
function redirectToSignup(req, res, next) {
/*jslint unparam:true*/
api.users.browse().then(function (users) {
if (users.length === 0) {
api.users.doesUserExist().then(function (exists) {
if (!exists) {
return res.redirect(config().paths.subdir + '/ghost/signup/');
}
next();

View file

@ -394,59 +394,24 @@ Post = ghostBookshelf.Model.extend({
.catch(errors.logAndThrowError);
},
permissable: function (postModelOrId, context, action_type, loadedPermissions) {
permissable: function (postModelOrId, context) {
var self = this,
userId = context.user,
isAuthor,
hasPermission,
userPermissions = loadedPermissions.user,
appPermissions = loadedPermissions.app,
postModel = postModelOrId,
checkPermission = function (perm) {
// Check for matching action type and object type
if (perm.get('action_type') !== action_type ||
perm.get('object_type') !== 'post') {
return false;
}
// 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;
};
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, context, action_type, loadedPermissions);
return this.read({id: postModelOrId, status: 'all'}).then(function (foundPostModel) {
return self.permissable(foundPostModel, context);
}, errors.logAndThrowError);
}
// Check if any permissions apply for this user and post.
hasPermission = _.any(userPermissions, checkPermission);
// If we have already have user permission and we passed in appPermissions check them
if (hasPermission && !_.isNull(appPermissions)) {
hasPermission = _.any(appPermissions, checkPermission);
}
// If this is the author of the post, allow it.
// Moved below the permissions checks because there may not be a postModel
// in the case like canThis(user).create.post()
isAuthor = (postModel && userId === postModel.get('author_id'));
hasPermission = hasPermission || isAuthor;
// Resolve if we have appropriate permissions
if (hasPermission) {
if (postModel && userId === postModel.get('author_id')) {
return when.resolve();
}
// Otherwise, you shall not pass.
return when.reject();
},
add: function (newPostData, options) {

View file

@ -140,6 +140,26 @@ User = ghostBookshelf.Model.extend({
},
permissable: function (userModelOrId, context) {
var self = this,
userId = context.user,
userModel = userModelOrId;
// If we passed in an id instead of a model, get the model
// then check the permissions
if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
return this.read({id: userModelOrId, status: 'all'}).then(function (foundUserModel) {
return self.permissable(foundUserModel, context);
}, errors.logAndThrowError);
}
// If this is the same user that requests the operation allow it.
if (userModel && userId === userModel.get('id')) {
return when.resolve();
}
return when.reject();
},
setWarning: function (user) {
var status = user.get('status'),
regexp = /warn-(\d+)/i,

View file

@ -79,10 +79,8 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
// It's a model, get the id
modelId = modelOrId.id;
}
// 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,
@ -111,22 +109,11 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
return modelId === permObjId;
};
// Allow for a target model to implement a "Permissable" interface
if (TargetModel && _.isFunction(TargetModel.permissable)) {
return TargetModel.permissable(modelId, context, act_type, loadedPermissions);
}
// Check user permissions for matching action, object and id.
if (!_.isEmpty(userPermissions)) {
hasUserPermission = _.any(userPermissions, checkPermission);
}
// If we already checked user permissions and they failed,
// no need to check app permissions
if (hasUserPermission === false) {
return when.reject();
}
// Check app permissions if they were passed
hasAppPermission = true;
if (!_.isNull(appPermissions)) {
@ -136,12 +123,11 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
if (hasUserPermission && hasAppPermission) {
return when.resolve();
}
return when.reject();
}).otherwise(function () {
// Still check for permissable without permissions
// Check for special permissions on the model directly
if (TargetModel && _.isFunction(TargetModel.permissable)) {
return TargetModel.permissable(modelId, context, act_type, []);
return TargetModel.permissable(modelId, context);
}
return when.reject();

View file

@ -3,10 +3,11 @@ var testUtils = require('../../utils'),
should = require('should'),
// Stuff we are testing
DataGenerator = require('../../utils/fixtures/data-generator'),
dbAPI = require('../../../server/api/db');
TagsAPI = require('../../../server/api/tags');
PostAPI = require('../../../server/api/posts');
permissions = require('../../../server/permissions'),
DataGenerator = require('../../utils/fixtures/data-generator'),
dbAPI = require('../../../server/api/db');
TagsAPI = require('../../../server/api/tags');
PostAPI = require('../../../server/api/posts');
describe('DB API', function () {
@ -33,8 +34,9 @@ describe('DB API', function () {
});
it('delete all content', function (done) {
dbAPI.deleteAllContent().then(function (result){
permissions.init().then(function () {
return dbAPI.deleteAllContent();
}).then(function (result){
should.exist(result.message);
result.message.should.equal('Successfully deleted all content from your blog.')
}).then(function () {
@ -48,6 +50,8 @@ describe('DB API', function () {
results.posts.length.should.equal(0);
done();
});
}).then(null, done);
}).otherwise(function () {
done()
});
});
});

View file

@ -3,8 +3,9 @@ var testUtils = require('../../utils'),
should = require('should'),
// Stuff we are testing
permissions = require('../../../server/permissions'),
DataGenerator = require('../../utils/fixtures/data-generator'),
UsersAPI = require('../../../server/api/users');
UsersAPI = require('../../../server/api/users');
describe('Users API', function () {
@ -31,11 +32,15 @@ describe('Users API', function () {
});
it('can browse', function (done) {
UsersAPI.browse().then(function (results) {
permissions.init().then(function () {
return UsersAPI.browse.call({user:1})
}).then(function (results) {
should.exist(results);
results.length.should.be.above(0);
testUtils.API.checkResponse(results[0], 'user');
done();
}).then(null, done);
}).otherwise(function () {
done();
});
});
});

View file

@ -421,7 +421,9 @@ describe("Import", function () {
assert.equal(new Date(posts[1].published_at).getTime(), timestamp);
done();
}).then(null, done);
}).otherwise(function (error) {
done(new Error(error));
})
});
it("doesn't import invalid post data from 002", function (done) {

View file

@ -268,17 +268,18 @@ describe('Permissions', function () {
return when.resolve();
});
// createTestUser()
UserProvider.browse()
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.browse();
})
.then(function (foundUser) {
testUser = foundUser.models[0];
testUser = foundUser.models[1];
return permissions.canThis(testUser).edit.post(123);
})
.then(function () {
permissableStub.restore();
permissableStub.calledWith(123, testUser.id, 'edit').should.equal(true);
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
done();
})
@ -296,10 +297,12 @@ describe('Permissions', function () {
return when.reject();
});
// createTestUser()
UserProvider.browse()
testUtils.insertAuthorUser()
.then(function () {
return UserProvider.browse();
})
.then(function (foundUser) {
testUser = foundUser.models[0];
testUser = foundUser.models[1];
return permissions.canThis(testUser).edit.post(123);
})
@ -310,7 +313,7 @@ describe('Permissions', function () {
})
.otherwise(function () {
permissableStub.restore();
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }, 'edit').should.equal(true);
permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true);
done();
});
});

View file

@ -99,6 +99,32 @@ function insertDefaultUser() {
});
}
function insertEditorUser() {
var users = [],
userRoles = [];
users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[1]));
userRoles.push(DataGenerator.forKnex.createUserRole(1, 2));
return knex('users')
.insert(users)
.then(function () {
return knex('roles_users').insert(userRoles);
});
}
function insertAuthorUser() {
var users = [],
userRoles = [];
users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[2]));
userRoles.push(DataGenerator.forKnex.createUserRole(1, 3));
return knex('users')
.insert(users)
.then(function () {
return knex('roles_users').insert(userRoles);
});
}
function insertDefaultApp() {
var apps = [];
@ -192,6 +218,8 @@ module.exports = {
insertMorePosts: insertMorePosts,
insertMorePostsTags: insertMorePostsTags,
insertDefaultUser: insertDefaultUser,
insertEditorUser: insertEditorUser,
insertAuthorUser: insertAuthorUser,
insertDefaultApp: insertDefaultApp,
insertApps: insertApps,
insertAppWithSettings: insertAppWithSettings,