diff --git a/core/server/api/db.js b/core/server/api/db.js index f3c59a17d0d..e0ee749c46c 100644 --- a/core/server/api/db.js +++ b/core/server/api/db.js @@ -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 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.'}); + } - 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 = ''; + 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")); - } + // 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")); - } + 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 !== "" ? "
" : ""; - error += err.message; - } + _.each(_.keys(importData.data), function (tableName) { + _.each(importData.data[tableName], function (importValues) { + try { + validation.validateSchema(tableName, importValues); + } catch (err) { + error += error !== "" ? "
" : ""; + error += err.message; + } + }); }); - }); - if (error !== "") { - return when.reject(new Error(error)); - } - // Import for the current version - return dataImport(databaseVersion, importData); + 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); + }).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)'}); + }); } }; diff --git a/core/server/api/index.js b/core/server/api/index.js index ed07d1e94d9..cbdc5ccb9f9 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -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, diff --git a/core/server/api/posts.js b/core/server/api/posts.js index 923afee7c1a..a3147442ecf 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -13,6 +13,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, @@ -29,9 +32,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) { @@ -45,15 +54,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 @@ -100,9 +100,11 @@ posts = { // **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 when(posts.read({id : args.id, status: 'all'})).then(function (result) { + // TODO: Would it be good to get rid of .call()? + return when(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; @@ -111,7 +113,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; \ No newline at end of file diff --git a/core/server/api/users.js b/core/server/api/users.js index e0efbe3afd6..adcd6c2370b 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -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; + }); } }; diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index 6a07b2447ab..b7e94ccb534 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -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' }; diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 7c220e03250..4ee2c785e06 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -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') diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index 285e4a71b17..b7580a7d3e2 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -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(); diff --git a/core/server/models/post.js b/core/server/models/post.js index 5c31bdabee9..2a9688404b2 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -397,59 +397,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) { diff --git a/core/server/models/user.js b/core/server/models/user.js index 017670db236..9e6de5226e9 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -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, diff --git a/core/server/permissions/index.js b/core/server/permissions/index.js index 81eacde073b..096e1e25f98 100644 --- a/core/server/permissions/index.js +++ b/core/server/permissions/index.js @@ -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(); diff --git a/core/test/functional/api/users_test.js b/core/test/functional/api/users_test.js index dd710da108b..7a2879b96c1 100644 --- a/core/test/functional/api/users_test.js +++ b/core/test/functional/api/users_test.js @@ -1,8 +1,8 @@ /*globals describe, before, beforeEach, afterEach, it */ var testUtils = require('../../utils'), - should = require('should'), - _ = require('lodash'), - request = require('request'); + should = require('should'), + _ = require('lodash'), + request = require('request'); request = request.defaults({jar:true}) diff --git a/core/test/integration/api/api_db_spec.js b/core/test/integration/api/api_db_spec.js index cb3c5a9753e..eb0ec2bd3db 100644 --- a/core/test/integration/api/api_db_spec.js +++ b/core/test/integration/api/api_db_spec.js @@ -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() + }); }); }); \ No newline at end of file diff --git a/core/test/integration/api/api_users_spec.js b/core/test/integration/api/api_users_spec.js index 9dfe4672484..cf117fa0489 100644 --- a/core/test/integration/api/api_users_spec.js +++ b/core/test/integration/api/api_users_spec.js @@ -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(); + }); }); }); \ No newline at end of file diff --git a/core/test/unit/import_spec.js b/core/test/unit/import_spec.js index 5956a1cadab..eee562cffc9 100644 --- a/core/test/unit/import_spec.js +++ b/core/test/unit/import_spec.js @@ -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) { diff --git a/core/test/unit/permissions_spec.js b/core/test/unit/permissions_spec.js index cf98d88eeb8..c7838156a06 100644 --- a/core/test/unit/permissions_spec.js +++ b/core/test/unit/permissions_spec.js @@ -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(); }); }); diff --git a/core/test/utils/index.js b/core/test/utils/index.js index b8247b72125..bfd2a3ee657 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -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,