Skip to content

Commit

Permalink
Add permissions to API
Browse files Browse the repository at this point in the history
closes TryGhost#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
  • Loading branch information
sebgie committed Apr 16, 2014
1 parent 61e94a6 commit e47e9c6
Show file tree
Hide file tree
Showing 15 changed files with 257 additions and 178 deletions.
146 changes: 82 additions & 64 deletions core/server/api/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 !== "" ? "<br>" : "";
error += err.message;
}
_.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);
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)'});
});
}
};

Expand Down
2 changes: 1 addition & 1 deletion core/server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 32 additions & 12 deletions core/server/api/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
67 changes: 47 additions & 20 deletions core/server/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.'});
});
},

Expand All @@ -52,30 +56,44 @@ 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
// **takes:** a json object representing a user
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
Expand Down Expand Up @@ -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;
});
}
};

Expand Down
2 changes: 1 addition & 1 deletion core/server/controllers/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
};
Expand Down
Loading

0 comments on commit e47e9c6

Please sign in to comment.