Merge branch 'apps-perms' into master

This commit is contained in:
Hannah Wolfe 2014-04-16 19:27:10 +01:00
commit 9c1fe62173
53 changed files with 2382 additions and 498 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

@ -47,17 +47,17 @@ requestHandler = function (apiMethod) {
return function (req, res) {
var options = _.extend(req.body, req.files, req.query, req.params),
apiContext = {
user: req.session && req.session.user
user: (req.session && req.session.user) ? req.session.user : null
};
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,8 +20,10 @@ 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,
omitted = result;
@ -35,10 +37,15 @@ posts = {
// #### Read
// **takes:** an identifier (id or slug?)
read: function read(args) {
// **returns:** a promise for a single post in a json object
read: function read(options) {
options = options || {};
if (!this.user) {
// only published posts for
options.status = 'published';
}
return dataProvider.Post.findOne(args).then(function (result) {
// **returns:** a promise for a single post in a json object
return dataProvider.Post.findOne(options).then(function (result) {
var omitted;
if (result) {
@ -51,26 +58,15 @@ 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) {
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({code: 403, message: 'You do not have permission to edit this post.'});
}
var self = this;
return canThis(self.user).edit.post(postData.id).then(function () {
return checkPostData(postData).then(function (checkedPostData) {
return dataProvider.Post.edit(checkedPostData.posts[0]);
return dataProvider.Post.edit(checkedPostData.posts[0], {user: self.user});
}).then(function (result) {
if (result) {
var omitted = result.toJSON();
@ -87,14 +83,11 @@ posts = {
// #### Add
// **takes:** a json object representing a post,
add: function add(postData) {
var self = this;
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({code: 403, message: 'You do not have permission to add posts.'});
}
return canThis(this.user).create.post().then(function () {
return checkPostData(postData).then(function (checkedPostData) {
return dataProvider.Post.add(checkedPostData.posts[0]);
return dataProvider.Post.add(checkedPostData.posts[0], {user: self.user});
}).then(function (result) {
var omitted = result.toJSON();
omitted.author = _.omit(omitted.author, filteredUserAttributes);
@ -108,13 +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
if (!this.user) {
return when.reject({code: 403, message: 'You do not have permission to remove posts.'});
}
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;
@ -123,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

@ -181,16 +181,19 @@ settings = {
// **takes:** either a json object representing a collection of settings, or a key and value pair
edit: function edit(key, value) {
var self = this,
type;
// Check for passing a collection of settings first
if (_.isObject(key)) {
//clean data
var type = key.type;
type = key.type;
delete key.type;
delete key.availableThemes;
delete key.availableApps;
key = settingsCollection(key);
return dataProvider.Settings.edit(key).then(function (result) {
return dataProvider.Settings.edit(key, {user: self.user}).then(function (result) {
result.models = result;
return when(readSettingsResult(result)).then(function (settings) {
updateSettingsCache(settings);
@ -216,7 +219,7 @@ settings = {
value = JSON.stringify(value);
}
setting.set('value', value);
return dataProvider.Settings.edit(setting).then(function (result) {
return dataProvider.Settings.edit(setting, {user: self.user}).then(function (result) {
settingsCache[_.first(result).attributes.key].value = _.first(result).attributes.value;
}).then(function () {
return config.theme.update(settings, config().url).then(function () {

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,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).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);
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: 'internal'}, userData);
},
// #### Check
@ -103,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

@ -26,7 +26,7 @@ function saveInstalledApps(installedApps) {
return getInstalledApps().then(function (currentInstalledApps) {
var updatedAppsInstalled = _.uniq(installedApps.concat(currentInstalledApps));
return api.settings.edit('installedApps', updatedAppsInstalled);
return api.settings.edit.call({user: 1}, 'installedApps', updatedAppsInstalled);
});
}

View File

@ -6,6 +6,7 @@ var path = require('path'),
config = require('../config'),
AppSandbox = require('./sandbox'),
AppDependencies = require('./dependencies'),
AppPermissions = require('./permissions'),
loader;
// Get the full path to an app by name
@ -48,37 +49,53 @@ loader = {
// Load a app and return the instantiated app
installAppByName: function (name) {
// Install the apps dependendencies first
var deps = new AppDependencies(getAppAbsolutePath(name));
return deps.install().then(function () {
var app = getAppByName(name);
var appPath = getAppAbsolutePath(name),
deps = new AppDependencies(appPath);
// Check for an install() method on the app.
if (!_.isFunction(app.install)) {
return when.reject(new Error("Error loading app named " + name + "; no install() method defined."));
}
return deps.install()
.then(function () {
// Load app permissions
var perms = new AppPermissions(appPath);
// Run the app.install() method
// Wrapping the install() with a when because it's possible
// to not return a promise from it.
return when(app.install(appProxy)).then(function () {
return when.resolve(app);
return perms.read().otherwise(function (err) {
// Provide a helpful error about which app
return when.reject(new Error("Error loading app named " + name + "; problem reading permissions: " + err.message));
});
})
.then(function (appPerms) {
var app = getAppByName(name, appPerms);
// Check for an install() method on the app.
if (!_.isFunction(app.install)) {
return when.reject(new Error("Error loading app named " + name + "; no install() method defined."));
}
// Run the app.install() method
// Wrapping the install() with a when because it's possible
// to not return a promise from it.
return when(app.install(appProxy)).then(function () {
return when.resolve(app);
});
});
});
},
// Activate a app and return it
activateAppByName: function (name) {
var app = getAppByName(name);
var perms = new AppPermissions(getAppAbsolutePath(name));
// Check for an activate() method on the app.
if (!_.isFunction(app.activate)) {
return when.reject(new Error("Error loading app named " + name + "; no activate() method defined."));
}
return perms.read().then(function (appPerms) {
var app = getAppByName(name, appPerms);
// Wrapping the activate() with a when because it's possible
// to not return a promise from it.
return when(app.activate(appProxy)).then(function () {
return when.resolve(app);
// Check for an activate() method on the app.
if (!_.isFunction(app.activate)) {
return when.reject(new Error("Error loading app named " + name + "; no activate() method defined."));
}
// Wrapping the activate() with a when because it's possible
// to not return a promise from it.
return when(app.activate(appProxy)).then(function () {
return when.resolve(app);
});
});
}
};

View File

@ -0,0 +1,75 @@
var fs = require('fs'),
when = require('when'),
path = require('path'),
parsePackageJson = require('../require-tree').parsePackageJson;
function AppPermissions(appPath) {
this.appPath = appPath;
this.packagePath = path.join(this.appPath, 'package.json');
}
AppPermissions.prototype.read = function () {
var self = this,
def = when.defer();
this.checkPackageContentsExists()
.then(function (exists) {
if (!exists) {
// If no package.json, return default permissions
return def.resolve(AppPermissions.DefaultPermissions);
}
// Read and parse the package.json
self.getPackageContents()
.then(function (parsed) {
// If no permissions in the package.json then return the default permissions.
if (!(parsed.ghost && parsed.ghost.permissions)) {
return def.resolve(AppPermissions.DefaultPermissions);
}
// TODO: Validation on permissions object?
def.resolve(parsed.ghost.permissions);
})
.otherwise(def.reject);
})
.otherwise(def.reject);
return def.promise;
};
AppPermissions.prototype.checkPackageContentsExists = function () {
// Mostly just broken out for stubbing in unit tests
var def = when.defer();
fs.exists(this.packagePath, function (exists) {
def.resolve(exists);
});
return def.promise;
};
// Get the contents of the package.json in the appPath root
AppPermissions.prototype.getPackageContents = function () {
var messages = {
errors: [],
warns: []
};
return parsePackageJson(this.packagePath, messages)
.then(function (parsed) {
if (!parsed) {
return when.reject(new Error(messages.errors[0].message));
}
return parsed;
});
};
// Default permissions for an App.
AppPermissions.DefaultPermissions = {
posts: ['browse', 'read']
};
module.exports = AppPermissions;

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'
};
@ -244,12 +244,12 @@ adminControllers = {
email = req.body.email,
password = req.body.password;
api.users.add({
api.users.register({
name: name,
email: email,
password: password
}).then(function (user) {
api.settings.edit('email', email).then(function () {
api.settings.edit.call({user: 1}, 'email', email).then(function () {
var message = {
to: email,
subject: 'Your New Ghost Blog',

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

@ -1,7 +1,7 @@
{
"core": {
"databaseVersion": {
"defaultValue": "002"
"defaultValue": "003"
},
"dbHash": {
"defaultValue": null

View File

@ -1,9 +1,13 @@
var sequence = require('when/sequence'),
_ = require('lodash'),
Post = require('../../models/post').Post,
Tag = require('../../models/tag').Tag,
Role = require('../../models/role').Role,
Permission = require('../../models/permission').Permission;
var sequence = require('when/sequence'),
_ = require('lodash'),
Post = require('../../models/post').Post,
Tag = require('../../models/tag').Tag,
Role = require('../../models/role').Role,
Permission = require('../../models/permission').Permission,
Permissions = require('../../models/permission').Permissions,
populateFixtures,
updateFixtures;
var fixtures = {
posts: [
@ -63,42 +67,226 @@ var fixtures = {
"action_type": "create",
"object_type": "post"
}
],
permissions003: [
{
"name": "Get slug",
"action_type": "slug",
"object_type": "post"
},
{
"name": "Export database",
"action_type": "exportContent",
"object_type": "db"
},
{
"name": "Import database",
"action_type": "importContent",
"object_type": "db"
},
{
"name": "Delete all content",
"action_type": "deleteAllContent",
"object_type": "db"
},
{
"name": "Browse users",
"action_type": "browse",
"object_type": "user"
},
{
"name": "Read users",
"action_type": "read",
"object_type": "user"
},
{
"name": "Edit users",
"action_type": "edit",
"object_type": "user"
},
{
"name": "Add users",
"action_type": "add",
"object_type": "user"
},
{
"name": "Browse settings",
"action_type": "browse",
"object_type": "setting"
},
{
"name": "Read settings",
"action_type": "read",
"object_type": "setting"
},
{
"name": "Edit settings",
"action_type": "edit",
"object_type": "setting"
}
]
};
module.exports = {
populateFixtures: function () {
var ops = [];
populateFixtures = function () {
var ops = [],
relations = [];
_.each(fixtures.posts, function (post) {
ops.push(function () {return Post.add(post); });
});
_.each(fixtures.posts, function (post) {
ops.push(function () {return Post.add(post, {user: 1}); });
});
_.each(fixtures.tags, function (tag) {
ops.push(function () {return Tag.add(tag); });
});
_.each(fixtures.tags, function (tag) {
ops.push(function () {return Tag.add(tag, {user: 1}); });
});
_.each(fixtures.roles, function (role) {
ops.push(function () {return Role.add(role); });
});
_.each(fixtures.permissions, function (permission) {
ops.push(function () {return Permission.add(permission); });
});
_.each(fixtures.roles, function (role) {
ops.push(function () {return Role.add(role, {user: 1}); });
});
// add the tag to the post
ops.push(function () {
Post.forge({id: 1}).fetch({withRelated: ['tags']}).then(function (post) {
post.tags().attach([1]);
_.each(fixtures.permissions, function (permission) {
ops.push(function () {return Permission.add(permission, {user: 1}); });
});
_.each(fixtures.permissions003, function (permission) {
ops.push(function () {return Permission.add(permission, {user: 1}); });
});
// add the tag to the post
relations.push(function () {
Post.forge({id: 1}).fetch({withRelated: ['tags']}).then(function (post) {
post.tags().attach([1]);
});
});
//grant permissions to roles
relations.push(function () {
// admins gets all permissions
Role.forge({name: 'Administrator'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var admin_perm = _.map(perms.toJSON(), function (perm) {
return perm.id;
});
return role.permissions().attach(_.compact(admin_perm));
});
});
// finally, grant admins all permissions
ops.push(function () {
Role.forge({id: 1}).fetch({withRelated: ['permissions']}).then(function (role) {
role.permissions().attach([1, 2, 3]);
// editor gets access to posts, users and settings.browse, settings.read
Role.forge({name: 'Editor'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var editor_perm = _.map(perms.toJSON(), function (perm) {
if (perm.object_type === 'post' || perm.object_type === 'user') {
return perm.id;
}
if (perm.object_type === 'setting' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
return null;
});
return role.permissions().attach(_.compact(editor_perm));
});
});
return sequence(ops);
}
// author gets access to post.add, post.slug, settings.browse, settings.read, users.browse and users.read
Role.forge({name: 'Author'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var author_perm = _.map(perms.toJSON(), function (perm) {
if (perm.object_type === 'post' &&
(perm.action_type === 'add' || perm.action_type === 'slug')) {
return perm.id;
}
if (perm.object_type === 'setting' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
if (perm.object_type === 'user' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
return null;
});
return role.permissions().attach(_.compact(author_perm));
});
});
});
return sequence(ops).then(function () {
sequence(relations);
});
};
updateFixtures = function () {
var ops = [],
relations = [];
_.each(fixtures.permissions003, function (permission) {
ops.push(function () {return Permission.add(permission, {user: 1}); });
});
relations.push(function () {
// admin gets all new permissions
Role.forge({name: 'Administrator'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var admin_perm = _.map(perms.toJSON(), function (perm) {
var result = fixtures.permissions003.filter(function (object) {
return object.object_type === perm.object_type && object.action_type === perm.action_type;
});
if (!_.isEmpty(result)) {
return perm.id;
}
return null;
});
return role.permissions().attach(_.compact(admin_perm));
});
});
// editor gets access to posts, users and settings.browse, settings.read
Role.forge({name: 'Editor'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var editor_perm = _.map(perms.toJSON(), function (perm) {
if (perm.object_type === 'post' || perm.object_type === 'user') {
return perm.id;
}
if (perm.object_type === 'setting' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
return null;
});
return role.permissions().attach(_.compact(editor_perm));
});
});
// author gets access to post.add, post.slug, settings.browse, settings.read, users.browse and users.read
Role.forge({name: 'Author'}).fetch({withRelated: ['permissions']}).then(function (role) {
Permissions.forge().fetch().then(function (perms) {
var author_perm = _.map(perms.toJSON(), function (perm) {
if (perm.object_type === 'post' &&
(perm.action_type === 'add' || perm.action_type === 'slug')) {
return perm.id;
}
if (perm.object_type === 'setting' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
if (perm.object_type === 'user' &&
(perm.action_type === 'browse' || perm.action_type === 'read')) {
return perm.id;
}
return null;
});
return role.permissions().attach(_.compact(author_perm));
});
});
});
return sequence(ops).then(function () {
sequence(relations);
});
};
module.exports = {
populateFixtures: populateFixtures,
updateFixtures: updateFixtures
};

View File

@ -12,7 +12,8 @@ Importer000 = function () {
this.importFrom = {
'000': this.basicImport,
'001': this.basicImport,
'002': this.basicImport
'002': this.basicImport,
'003': this.basicImport
};
};
@ -35,6 +36,7 @@ Importer000.prototype.canImport = function (data) {
function stripProperties(properties, data) {
data = _.clone(data, true);
_.each(data, function (obj) {
_.each(properties, function (property) {
delete obj[property];
@ -82,7 +84,7 @@ function importTags(ops, tableData, transaction) {
_.each(tableData, function (tag) {
ops.push(models.Tag.findOne({name: tag.name}, {transacting: transaction}).then(function (_tag) {
if (!_tag) {
return models.Tag.add(tag, {transacting: transaction})
return models.Tag.add(tag, {user: 1, transacting: transaction})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); });
}
@ -94,7 +96,7 @@ function importTags(ops, tableData, transaction) {
function importPosts(ops, tableData, transaction) {
tableData = stripProperties(['id'], tableData);
_.each(tableData, function (post) {
ops.push(models.Post.add(post, {transacting: transaction, importing: true})
ops.push(models.Post.add(post, {user: 1, transacting: transaction, importing: true})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); }));
});
@ -104,7 +106,7 @@ function importUsers(ops, tableData, transaction) {
// don't override the users credentials
tableData = stripProperties(['id', 'email', 'password'], tableData);
tableData[0].id = 1;
ops.push(models.User.edit(tableData[0], {transacting: transaction})
ops.push(models.User.edit(tableData[0], {user: 1, transacting: transaction})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); }));
}
@ -119,12 +121,52 @@ function importSettings(ops, tableData, transaction) {
tableData = _.filter(tableData, function (data) {
return blackList.indexOf(data.type) === -1;
});
ops.push(models.Settings.edit(tableData, transaction)
ops.push(models.Settings.edit(tableData, {user: 1, transacting: transaction})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); }));
}
function importApps(ops, tableData, transaction) {
tableData = stripProperties(['id'], tableData);
_.each(tableData, function (app) {
// Avoid duplicates
ops.push(models.App.findOne({name: app.name}, {transacting: transaction}).then(function (_app) {
if (!_app) {
return models.App.add(app, {transacting: transaction})
// add pass-through error handling so that bluebird doesn't think we've dropped it
.otherwise(function (error) { return when.reject(error); });
}
return when.resolve(_app);
}));
});
}
// function importAppSettings(ops, tableData, transaction) {
// var appsData = tableData.apps,
// appSettingsData = tableData.app_settings,
// appName;
//
// appSettingsData = stripProperties(['id'], appSettingsData);
//
// _.each(appSettingsData, function (appSetting) {
// // Find app to attach settings to
// appName = _.find(appsData, function (app) {
// return app.id === appSetting.app_id;
// }).name;
// ops.push(models.App.findOne({name: appName}, {transacting: transaction}).then(function (_app) {
// if (_app) {
// // Fix app_id
// appSetting.app_id = _app.id;
// return models.AppSetting.add(appSetting, {transacting: transaction})
// // add pass-through error handling so that bluebird doesn't think we've dropped it
// .otherwise(function (error) { return when.reject(error); });
// }
// // Gracefully ignore missing apps
// return when.resolve(_app);
// }));
// });
// }
// No data needs modifying, we just import whatever tables are available
Importer000.prototype.basicImport = function (data) {
var ops = [],
@ -153,6 +195,20 @@ Importer000.prototype.basicImport = function (data) {
importSettings(ops, tableData.settings, t);
}
if (tableData.apps && tableData.apps.length) {
importApps(ops, tableData.apps, t);
// ToDo: This is rather complicated
// Only import settings if there are apps defined
//if (tableData.app_settings && tableData.app_settings.length) {
// importAppSettings(ops, _.pick(tableData, 'apps', 'app_settings'), t);
//}
//if (tableData.app_fields && tableData.app_fields.length) {
// importAppFields(ops, _.pick(tableData, 'apps', 'posts', 'app_fields'), t);
//}
}
/** do nothing with these tables, the data shouldn't have changed from the fixtures
* permissions
* roles
@ -177,10 +233,10 @@ Importer000.prototype.basicImport = function (data) {
rej = true;
}
});
if (rej) {
t.rollback(error);
} else {
if (!rej) {
t.commit();
} else {
t.rollback(error);
}
});
}).then(function () {

View File

@ -0,0 +1,8 @@
var Importer000 = require('./000');
module.exports = {
Importer003: Importer000,
importData: function (data) {
return new Importer000.importData(data);
}
};

View File

@ -252,7 +252,7 @@ function checkMySQLPostTable() {
// Migrate from a specific version to the latest
migrateUp = function () {
return getTables().then(function (oldTables) {
// if tables exist and lient is mysqls check if posts table is okay
// if tables exist and client is mysqls check if posts table is okay
if (!_.isEmpty(oldTables) && client === 'mysql') {
return checkMySQLPostTable().then(function () {
return oldTables;
@ -274,6 +274,8 @@ migrateUp = function () {
return sequence(commands);
}
return;
}).then(function () {
return fixtures.updateFixtures();
});
};

View File

@ -81,6 +81,11 @@ var db = {
role_id: {type: 'integer', nullable: false},
permission_id: {type: 'integer', nullable: false}
},
permissions_apps: {
id: {type: 'increments', nullable: false, primary: true},
app_id: {type: 'integer', nullable: false},
permission_id: {type: 'integer', nullable: false}
},
sessions: {
id: {type: 'string', nullable: false, primary: true},
expires: {type: 'bigInteger', nullable: false},
@ -115,12 +120,49 @@ var db = {
id: {type: 'increments', nullable: false, primary: true},
post_id: {type: 'integer', nullable: false, unsigned: true, references: 'id', inTable: 'posts'},
tag_id: {type: 'integer', nullable: false, unsigned: true, references: 'id', inTable: 'tags'}
},
apps: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
name: {type: 'string', maxlength: 150, nullable: false, unique: true},
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
version: {type: 'string', maxlength: 150, nullable: false},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'inactive'},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'integer', nullable: true}
},
app_settings: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
key: {type: 'string', maxlength: 150, nullable: false, unique: true},
value: {type: 'text', maxlength: 65535, nullable: true},
app_id: {type: 'integer', nullable: false, unsigned: true, references: 'id', inTable: 'apps'},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'integer', nullable: true}
},
app_fields: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
key: {type: 'string', maxlength: 150, nullable: false},
value: {type: 'text', maxlength: 65535, nullable: true},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'html'},
app_id: {type: 'integer', nullable: false, unsigned: true, references: 'id', inTable: 'apps'},
relatable_id: {type: 'integer', nullable: false, unsigned: true},
relatable_type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'posts'},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'integer', nullable: true}
}
};
function isPost(jsonData) {
return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown')
&& jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') &&
jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
}
function isTag(jsonData) {

View File

@ -60,7 +60,7 @@ function initDbHashAndFirstRun() {
if (dbHash === null) {
var initHash = uuid.v4();
return when(api.settings.edit('dbHash', initHash)).then(function (settings) {
return when(api.settings.edit.call({user: 1}, 'dbHash', initHash)).then(function (settings) {
dbHash = settings.dbHash;
return dbHash;
}).then(doFirstRun);

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();

26
core/server/models/app.js Normal file
View File

@ -0,0 +1,26 @@
var ghostBookshelf = require('./base'),
AppSetting = require('./appSetting'),
App,
Apps;
App = ghostBookshelf.Model.extend({
tableName: 'apps',
permissions: function () {
// Have to use the require here because of circular dependencies
return this.belongsToMany(require('./permission').Permission, 'permissions_apps');
},
settings: function () {
return this.belongsToMany(AppSetting, 'app_settings');
}
});
Apps = ghostBookshelf.Collection.extend({
model: App
});
module.exports = {
App: App,
Apps: Apps
};

View File

@ -0,0 +1,21 @@
var ghostBookshelf = require('./base'),
Post = require('./post').Post,
AppField,
AppFields;
AppField = ghostBookshelf.Model.extend({
tableName: 'app_fields',
post: function () {
return this.morphOne(Post, 'relatable');
}
});
AppFields = ghostBookshelf.Collection.extend({
model: AppField
});
module.exports = {
AppField: AppField,
AppFields: AppFields
};

View File

@ -0,0 +1,21 @@
var ghostBookshelf = require('./base'),
App = require('./app'),
AppSetting,
AppSettings;
AppSetting = ghostBookshelf.Model.extend({
tableName: 'app_settings',
app: function () {
return this.belongsTo(App);
}
});
AppSettings = ghostBookshelf.Collection.extend({
model: AppSetting
});
module.exports = {
AppSetting: AppSetting,
AppSettings: AppSettings
};

View File

@ -47,20 +47,16 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
validation.validateSchema(this.tableName, this.toJSON());
},
creating: function () {
creating: function (newObj, attr, options) {
if (!this.get('created_by')) {
this.set('created_by', 1);
this.set('created_by', options.user);
}
},
saving: function () {
// Remove any properties which don't belong on the model
saving: function (newObj, attr, options) {
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
// sessions do not have 'updated_by' column
if (this.tableName !== 'sessions') {
this.set('updated_by', 1);
}
this.set('updated_by', options.user);
},
// Base prototype properties will go here

View File

@ -11,6 +11,9 @@ module.exports = {
Tag: require('./tag').Tag,
Base: require('./base'),
Session: require('./session').Session,
App: require('./app').App,
AppField: require('./appField').AppField,
AppSetting: require('./appSetting').AppSetting,
init: function () {
return migrations.init();

View File

@ -1,6 +1,7 @@
var ghostBookshelf = require('./base'),
User = require('./user').User,
Role = require('./role').Role,
App = require('./app').App,
Permission,
Permissions;
@ -15,6 +16,10 @@ Permission = ghostBookshelf.Model.extend({
users: function () {
return this.belongsToMany(User);
},
apps: function () {
return this.belongsToMany(App);
}
});

View File

@ -5,6 +5,7 @@ var _ = require('lodash'),
Showdown = require('showdown'),
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
converter = new Showdown.converter({extensions: [ghostgfm]}),
AppField = require('./appField').AppField,
User = require('./user').User,
Tag = require('./tag').Tag,
Tags = require('./tag').Tags,
@ -52,6 +53,7 @@ Post = ghostBookshelf.Model.extend({
tagsToCheck,
i;
options = options || {};
// keep tags for 'saved' event and deduplicate upper/lowercase tags
tagsToCheck = this.get('tags');
this.myTags = [];
@ -64,7 +66,7 @@ Post = ghostBookshelf.Model.extend({
self.myTags.push(item);
});
ghostBookshelf.Model.prototype.saving.call(this);
ghostBookshelf.Model.prototype.saving.call(this, newPage, attr, options);
this.set('html', converter.makeHtml(this.get('markdown')));
@ -77,7 +79,7 @@ Post = ghostBookshelf.Model.extend({
this.set('published_at', new Date());
}
// This will need to go elsewhere in the API layer.
this.set('published_by', 1);
this.set('published_by', options.user);
}
if (this.hasChanged('slug') || !this.get('slug')) {
@ -93,13 +95,14 @@ Post = ghostBookshelf.Model.extend({
creating: function (newPage, attr, options) {
/*jshint unused:false*/
options = options || {};
// set any dynamic default properties
if (!this.get('author_id')) {
this.set('author_id', 1);
this.set('author_id', options.user);
}
ghostBookshelf.Model.prototype.creating.call(this);
ghostBookshelf.Model.prototype.creating.call(this, newPage, attr, options);
},
updateTags: function (newPost, attr, options) {
@ -127,7 +130,9 @@ Post = ghostBookshelf.Model.extend({
});
if (tagsToDetach.length > 0) {
tagOperations.push(newPost.tags().detach(tagsToDetach, options));
// _.omit(options, 'query') is a fix for using bookshelf 0.6.8
// (https://github.com/tgriesser/bookshelf/issues/294)
tagOperations.push(newPost.tags().detach(tagsToDetach, _.omit(options, 'query')));
}
// Next check if new tags are all exactly the same as what is set on the model
@ -141,7 +146,9 @@ Post = ghostBookshelf.Model.extend({
if (!_.isEmpty(tagsToAttach)) {
return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch(options).then(function (matchingTags) {
_.each(matchingTags.toJSON(), function (matchingTag) {
tagOperations.push(newPost.tags().attach(matchingTag.id, options));
// _.omit(options, 'query') is a fix for using bookshelf 0.6.8
// (https://github.com/tgriesser/bookshelf/issues/294)
tagOperations.push(newPost.tags().attach(matchingTag.id, _.omit(options, 'query')));
tagsToAttach = _.reject(tagsToAttach, function (tagToAttach) {
return tagToAttach.name === matchingTag.name;
});
@ -170,7 +177,9 @@ Post = ghostBookshelf.Model.extend({
// Attach each newly created tag
_.each(createdTagsToAttach, function (tagToAttach) {
newPost.tags().attach(tagToAttach.id, tagToAttach.name, options);
// _.omit(options, 'query') is a fix for using bookshelf 0.6.8
// (https://github.com/tgriesser/bookshelf/issues/294)
newPost.tags().attach(tagToAttach.id, tagToAttach.name, _.omit(options, 'query'));
});
}
@ -198,6 +207,10 @@ Post = ghostBookshelf.Model.extend({
tags: function () {
return this.belongsToMany(Tag);
},
fields: function () {
return this.morphMany(AppField, 'relatable');
}
}, {
@ -206,7 +219,8 @@ Post = ghostBookshelf.Model.extend({
// Extends base model findAll to eager-fetch author and user relationships.
findAll: function (options) {
options = options || {};
options.withRelated = [ 'author', 'tags' ];
options.withRelated = [ 'author', 'tags', 'fields' ];
return ghostBookshelf.Model.findAll.call(this, options);
},
@ -223,7 +237,7 @@ Post = ghostBookshelf.Model.extend({
delete args.status;
}
options.withRelated = [ 'author', 'tags' ];
options.withRelated = [ 'author', 'tags', 'fields' ];
return ghostBookshelf.Model.findOne.call(this, args, options);
},
@ -289,7 +303,7 @@ Post = ghostBookshelf.Model.extend({
}
// Fetch related models
opts.withRelated = [ 'author', 'tags' ];
opts.withRelated = [ 'author', 'tags', 'fields' ];
// If a query param for a tag is attached
// we need to fetch the tag model to find its id
@ -380,52 +394,29 @@ Post = ghostBookshelf.Model.extend({
.catch(errors.logAndThrowError);
},
permissable: function (postModelOrId, userId, action_type, userPermissions) {
permissable: function (postModelOrId, context) {
var self = this,
hasPermission,
userId = context.user,
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, userId, action_type, userPermissions);
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, 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;
});
// 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()
hasPermission = hasPermission || (postModel && userId === postModel.get('author_id'));
// 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) {
var self = this;
options = options || {};
return ghostBookshelf.Model.add.call(this, newPostData, options).then(function (post) {
return self.findOne({status: 'all', id: post.id}, options);
@ -433,6 +424,7 @@ Post = ghostBookshelf.Model.extend({
},
edit: function (editedPost, options) {
var self = this;
options = options || {};
return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (post) {
if (post) {
@ -442,6 +434,7 @@ Post = ghostBookshelf.Model.extend({
},
destroy: function (_identifier, options) {
options = options || {};
return this.forge({id: _identifier}).fetch({withRelated: ['tags']}).then(function destroyTags(post) {
var tagIds = _.pluck(post.related('tags').toJSON(), 'id');
if (tagIds) {

View File

@ -5,7 +5,21 @@ var ghostBookshelf = require('./base'),
Session = ghostBookshelf.Model.extend({
tableName: 'sessions'
tableName: 'sessions',
// override for base function since we don't have
// a created_by field for sessions
creating: function (newObj, attr, options) {
/*jshint unused:false*/
},
// override for base function since we don't have
// a updated_by field for sessions
saving: function (newObj, attr, options) {
/*jshint unused:false*/
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
},
}, {
destroyAll: function (options) {

View File

@ -48,7 +48,6 @@ Settings = ghostBookshelf.Model.extend({
saving: function () {
// disabling sanitization until we can implement a better version
// All blog setting keys that need their values to be escaped.
// if (this.get('type') === 'blog' && _.contains(['title', 'description', 'email'], this.get('key'))) {
@ -69,20 +68,22 @@ Settings = ghostBookshelf.Model.extend({
});
},
edit: function (_data, t) {
var settings = this;
edit: function (_data, options) {
if (!Array.isArray(_data)) {
_data = [_data];
}
return when.map(_data, function (item) {
// Accept an array of models as input
if (item.toJSON) { item = item.toJSON(); }
return settings.forge({ key: item.key }).fetch({transacting: t}).then(function (setting) {
return Settings.forge({ key: item.key }).fetch(options).then(function (setting) {
if (setting) {
return setting.set('value', item.value).save(null, {transacting: t});
return setting.save({value: item.value}, options);
}
return settings.forge({ key: item.key, value: item.value }).save(null, {transacting: t});
return Settings.forge({ key: item.key, value: item.value }).save(null, options);
}, errors.logAndThrowError);
});
@ -101,7 +102,7 @@ Settings = ghostBookshelf.Model.extend({
}
if (isMissingFromDB) {
defaultSetting.value = defaultSetting.defaultValue;
insertOperations.push(Settings.forge(defaultSetting).save());
insertOperations.push(Settings.forge(defaultSetting).save(null, {user: 1}));
}
});

View File

@ -81,7 +81,7 @@ User = ghostBookshelf.Model.extend({
*
* Hashes the password provided before saving to the database.
*/
add: function (_user) {
add: function (_user, options) {
var self = this,
// Clone the _user so we don't expose the hashed password unnecessarily
@ -108,7 +108,7 @@ User = ghostBookshelf.Model.extend({
return self.gravatarLookup(userData);
}).then(function (userData) {
// Save the user with the hashed password
return ghostBookshelf.Model.add.call(self, userData);
return ghostBookshelf.Model.add.call(self, userData, options);
}).then(function (addedUser) {
// Assign the userData to our created user so we can pass it back
userData = addedUser;
@ -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,
@ -334,35 +354,6 @@ User = ghostBookshelf.Model.extend({
});
},
effectivePermissions: function (id) {
return this.read({id: id}, { withRelated: ['permissions', 'roles.permissions'] })
.then(function (foundUser) {
var seenPerms = {},
rolePerms = _.map(foundUser.related('roles').models, function (role) {
return role.related('permissions').models;
}),
allPerms = [];
rolePerms.push(foundUser.related('permissions').models);
_.each(rolePerms, function (rolePermGroup) {
_.each(rolePermGroup, function (perm) {
var key = perm.get('action_type') + '-' + perm.get('object_type') + '-' + perm.get('object_id');
// Only add perms once
if (seenPerms[key]) {
return;
}
allPerms.push(perm);
seenPerms[key] = true;
});
});
return when.resolve(allPerms);
}, errors.logAndThrowError);
},
gravatarLookup: function (userData) {
var gravatarUrl = '//www.gravatar.com/avatar/' +
crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') +

View File

@ -0,0 +1,49 @@
var _ = require('lodash'),
Models = require('../models'),
errors = require('../errorHandling'),
User = Models.User,
App = Models.App;
var effective = {
user: function (id) {
return User.read({id: id}, { withRelated: ['permissions', 'roles.permissions'] })
.then(function (foundUser) {
var seenPerms = {},
rolePerms = _.map(foundUser.related('roles').models, function (role) {
return role.related('permissions').models;
}),
allPerms = [];
rolePerms.push(foundUser.related('permissions').models);
_.each(rolePerms, function (rolePermGroup) {
_.each(rolePermGroup, function (perm) {
var key = perm.get('action_type') + '-' + perm.get('object_type') + '-' + perm.get('object_id');
// Only add perms once
if (seenPerms[key]) {
return;
}
allPerms.push(perm);
seenPerms[key] = true;
});
});
return allPerms;
}, errors.logAndThrowError);
},
app: function (appName) {
return App.read({name: appName}, { withRelated: ['permissions'] })
.then(function (foundApp) {
if (!foundApp) {
return [];
}
return foundApp.related('permissions').models;
}, errors.logAndThrowError);
}
};
module.exports = effective;

View File

@ -5,7 +5,7 @@ var _ = require('lodash'),
when = require('when'),
Models = require('../models'),
objectTypeModelMap = require('./objectTypeModelMap'),
UserProvider = Models.User,
effectivePerms = require('./effective'),
PermissionsProvider = Models.Permission,
init,
refresh,
@ -22,17 +22,44 @@ function hasActionsMap() {
});
}
// TODO: Move this to its own file so others can use it?
function parseContext(context) {
// Parse what's passed to canThis.beginCheck for standard user and app scopes
var parsed = {
internal: false,
user: null,
app: null
};
if (context && (context === 'internal' || context.internal)) {
parsed.internal = true;
}
// @TODO: Refactor canThis() references to pass { user: id } explicitly instead of primitives.
if (context && context.id) {
// Handle passing of just user.id string
parsed.user = context.id;
} else if (_.isNumber(context)) {
// Handle passing of just user id number
parsed.user = context;
} else if (_.isObject(context)) {
// Otherwise, use the new hotness { user: id, app: id } format
parsed.user = context.user;
parsed.app = context.app;
}
return parsed;
}
// Base class for canThis call results
CanThisResult = function () {
this.userPermissionLoad = false;
return;
};
CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type, userId) {
var self = this,
obj_type_handlers = {};
CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type, context, permissionLoad) {
// Iterate through the object types, i.e. ['post', 'tag', 'user']
_.each(obj_types, function (obj_type) {
return _.reduce(obj_types, function (obj_type_handlers, obj_type) {
// Grab the TargetModel through the objectTypeModelMap
var TargetModel = objectTypeModelMap[obj_type];
// Create the 'handler' for the object type;
@ -40,6 +67,11 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type,
obj_type_handlers[obj_type] = function (modelOrId) {
var modelId;
// If it's an internal request, resolve immediately
if (context.internal) {
return when.resolve();
}
if (_.isNumber(modelOrId) || _.isString(modelOrId)) {
// It's an id already, do nothing
modelId = modelOrId;
@ -47,80 +79,108 @@ 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 self.userPermissionLoad.then(function (userPermissions) {
return permissionLoad.then(function (loadedPermissions) {
// Iterate through the user permissions looking for an affirmation
var hasPermission;
var userPermissions = loadedPermissions.user,
appPermissions = loadedPermissions.app,
hasUserPermission,
hasAppPermission,
checkPermission = function (perm) {
var permObjId;
// Allow for a target model to implement a "Permissable" interface
if (TargetModel && _.isFunction(TargetModel.permissable)) {
return TargetModel.permissable(modelId, userId, act_type, userPermissions);
// Look for a matching action type and object type first
if (perm.get('action_type') !== act_type || perm.get('object_type') !== obj_type) {
return false;
}
// Grab the object id (if specified, could be null)
permObjId = perm.get('object_id');
// If we didn't specify a model (any thing)
// or the permission didn't have an id scope set
// then the "thing" has permission
if (!modelId || !permObjId) {
return true;
}
// Otherwise, check if the id's match
// TODO: String vs Int comparison possibility here?
return modelId === permObjId;
};
// Check user permissions for matching action, object and id.
if (!_.isEmpty(userPermissions)) {
hasUserPermission = _.any(userPermissions, checkPermission);
}
// Otherwise, check all the permissions for matching object id
hasPermission = _.any(userPermissions, function (userPermission) {
var permObjId;
// Check app permissions if they were passed
hasAppPermission = true;
if (!_.isNull(appPermissions)) {
hasAppPermission = _.any(appPermissions, checkPermission);
}
// Look for a matching action type and object type first
if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) {
return false;
}
// Grab the object id (if specified, could be null)
permObjId = userPermission.get('object_id');
// If we didn't specify a model (any thing)
// or the permission didn't have an id scope set
// then the user has permission
if (!modelId || !permObjId) {
return true;
}
// Otherwise, check if the id's match
// TODO: String vs Int comparison possibility here?
return modelId === permObjId;
});
if (hasPermission) {
if (hasUserPermission && hasAppPermission) {
return when.resolve();
}
return when.reject();
}).otherwise(function () {
// No permissions loaded, or error loading permissions
// Still check for permissable without permissions
// Check for special permissions on the model directly
if (TargetModel && _.isFunction(TargetModel.permissable)) {
return TargetModel.permissable(modelId, userId, act_type, []);
return TargetModel.permissable(modelId, context);
}
return when.reject();
});
};
});
return obj_type_handlers;
return obj_type_handlers;
}, {});
};
CanThisResult.prototype.beginCheck = function (user) {
CanThisResult.prototype.beginCheck = function (context) {
var self = this,
userId = user.id || user;
userPermissionLoad,
appPermissionLoad,
permissionsLoad;
// Get context.user and context.app
context = parseContext(context);
if (!hasActionsMap()) {
throw new Error("No actions map found, please call permissions.init() before use.");
}
// TODO: Switch logic based on object type; user, role, post.
// Kick off loading of effective user permissions if necessary
if (context.user) {
userPermissionLoad = effectivePerms.user(context.user);
} else {
// Resolve null if no context.user to prevent db call
userPermissionLoad = when.resolve(null);
}
// Kick off the fetching of the user data
this.userPermissionLoad = UserProvider.effectivePermissions(userId);
// Kick off loading of effective app permissions if necessary
if (context.app) {
appPermissionLoad = effectivePerms.app(context.app);
} else {
// Resolve null if no context.app
appPermissionLoad = when.resolve(null);
}
// Wait for both user and app permissions to load
permissionsLoad = when.all([userPermissionLoad, appPermissionLoad]).then(function (result) {
return {
user: result[0],
app: result[1]
};
});
// Iterate through the actions and their related object types
_.each(exported.actionsMap, function (obj_types, act_type) {
// Build up the object type handlers;
// the '.post()' parts in canThis(user).edit.post()
var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type, userId);
var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type, context, permissionsLoad);
// Define a property for the action on the result;
// the '.edit' in canThis(user).edit.post()
@ -136,10 +196,10 @@ CanThisResult.prototype.beginCheck = function (user) {
return this;
};
canThis = function (user) {
canThis = function (context) {
var result = new CanThisResult();
return result.beginCheck(user);
return result.beginCheck(context);
};
init = refresh = function () {

View File

@ -143,10 +143,10 @@ function updateCheckRequest() {
function updateCheckResponse(response) {
var ops = [];
ops.push(api.settings.edit('nextUpdateCheck', response.next_check)
ops.push(api.settings.edit.call({user: 1}, 'nextUpdateCheck', response.next_check)
.otherwise(errors.rejectError));
ops.push(api.settings.edit('displayUpdateNotification', response.version)
ops.push(api.settings.edit.call({user: 1}, 'displayUpdateNotification', response.version)
.otherwise(errors.rejectError));
return when.settle(ops).then(function (descriptors) {

View File

@ -591,8 +591,6 @@ describe('Post API', function () {
.send(newPost)
.expect(200)
.end(function (err ,res) {
console.log("end");
console.log(err);
if (err) {
return done(err);
}

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 () {
@ -17,13 +18,15 @@ describe('DB API', function () {
});
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return testUtils.insertDefaultFixtures();
})
.then(function () {
done();
}, done);
testUtils.initData().then(function () {
return testUtils.insertDefaultFixtures();
}).then(function () {
return testUtils.insertEditorUser();
}).then(function () {
return testUtils.insertAuthorUser();
}).then(function () {
done();
}, done);
});
afterEach(function (done) {
@ -33,8 +36,9 @@ describe('DB API', function () {
});
it('delete all content', function (done) {
dbAPI.deleteAllContent().then(function (result){
permissions.init().then(function () {
return dbAPI.deleteAllContent.call({user: 1});
}).then(function (result){
should.exist(result.message);
result.message.should.equal('Successfully deleted all content from your blog.')
}).then(function () {
@ -48,6 +52,71 @@ describe('DB API', function () {
results.posts.length.should.equal(0);
done();
});
}).then(null, done);
}).otherwise(function (error) {
done(new Error(JSON.stringify(error)));
});
});
it('delete all content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.deleteAllContent.call({user: 2});
}).then(function (){
done(new Error("Delete all content is not denied for editor."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.deleteAllContent.call({user: 3});
}).then(function (){
done(new Error("Delete all content is not denied for author."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.deleteAllContent();
}).then(function (){
done(new Error("Delete all content is not denied without authentication."));
}, function (error) {
error.code.should.eql(403);
done();
});
});
it('export content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.exportContent.call({user: 2});
}).then(function (){
done(new Error("Export content is not denied for editor."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.exportContent.call({user: 3});
}).then(function (){
done(new Error("Export content is not denied for author."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.exportContent();
}).then(function (){
done(new Error("Export content is not denied without authentication."));
}, function (error) {
error.code.should.eql(403);
done();
});
});
it('import content is denied', function (done) {
permissions.init().then(function () {
return dbAPI.importContent.call({user: 2});
}).then(function (result){
done(new Error("Import content is not denied for editor."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.importContent.call({user: 3});
}).then(function (result){
done(new Error("Import content is not denied for author."));
}, function (error) {
error.code.should.eql(403);
return dbAPI.importContent();
}).then(function (result){
done(new Error("Import content is not denied without authentication."));
}, function (error) {
error.code.should.eql(403);
done();
});
});
});

View File

@ -30,7 +30,7 @@ describe('Post API', function () {
}, done);
});
it('can browse', function (done) {
it('browse', function (done) {
PostAPI.browse().then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'posts');
@ -41,7 +41,7 @@ describe('Post API', function () {
}).then(null, done);
});
it('can read', function (done) {
it('read', function (done) {
var firstPost;
PostAPI.browse().then(function (results) {

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 () {
@ -15,13 +16,15 @@ describe('Users API', function () {
});
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return testUtils.insertDefaultFixtures();
})
.then(function () {
done();
}, done);
testUtils.initData().then(function () {
return testUtils.insertDefaultFixtures();
}).then(function () {
return testUtils.insertEditorUser();
}).then(function () {
return testUtils.insertAuthorUser();
}).then(function () {
done();
}, done);
});
afterEach(function (done) {
@ -30,12 +33,77 @@ describe('Users API', function () {
}, done);
});
it('can browse', function (done) {
UsersAPI.browse().then(function (results) {
it('browse', function (done) {
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');
}, function (error) {
done(new Error(JSON.stringify(error)));
}).then(function () {
return UsersAPI.browse.call({user: 2});
}).then(function (results) {
should.exist(results);
results.length.should.be.above(0);
testUtils.API.checkResponse(results[0], 'user');
}, function (error) {
done(new Error(JSON.stringify(error)));
}).then(function () {
return UsersAPI.browse.call({user: 3});
}).then(function (results) {
should.exist(results);
results.length.should.be.above(0);
testUtils.API.checkResponse(results[0], 'user');
done();
}).then(null, done);
}, function (error) {
done(new Error(JSON.stringify(error)));
})
});
it('browse denied', function (done) {
permissions.init().then(function () {
return UsersAPI.browse();
}).then(function (results) {
done(new Error("Browse user is not denied without authentication."));
}, function () {
done();
});
});
it('read', function (done) {
permissions.init().then(function () {
return UsersAPI.read.call({user: 1}, {id: 1});
}).then(function (result) {
should.exist(result);
result.id.should.eql(1);
testUtils.API.checkResponse(result, 'user');
}, function (error) {
done(new Error(JSON.stringify(error)));
}).then(function () {
return UsersAPI.read.call({user: 2}, {id: 1});
}).then(function (result) {
should.exist(result);
result.id.should.eql(1);
testUtils.API.checkResponse(result, 'user');
}, function (error) {
done(new Error(JSON.stringify(error)));
}).then(function () {
return UsersAPI.read.call({user: 3}, {id: 1});
}).then(function (result) {
should.exist(result);
result.id.should.eql(1);
testUtils.API.checkResponse(result, 'user');
}, function (error) {
done(new Error(JSON.stringify(error)));
}).then(function () {
return UsersAPI.read({id: 1});
}).then(function (result) {
should.exist(result);
result.id.should.eql(1);
testUtils.API.checkResponse(result, 'user');
done();
}, function (error) {
done(new Error(JSON.stringify(error)));
});
});
});

View File

@ -0,0 +1,76 @@
/*globals describe, before, beforeEach, afterEach, it*/
var testUtils = require('../../utils'),
should = require('should'),
_ = require("lodash"),
// Stuff we are testing
Models = require('../../../server/models'),
knex = require('../../../server/models/base').knex;
describe('App Fields Model', function () {
var AppFieldsModel = Models.AppField;
before(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return testUtils.insertApps();
})
.then(function () {
done();
}, done);
});
afterEach(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
after(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
it('can browse', function (done) {
AppFieldsModel.browse().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
done();
}).then(null, done);
});
it('can read', function (done) {
AppFieldsModel.read({id: 1}).then(function (foundAppField) {
should.exist(foundAppField);
done();
}).then(null, done);
});
it('can edit', function (done) {
AppFieldsModel.read({id: 1}).then(function (foundAppField) {
should.exist(foundAppField);
return foundAppField.set({value: "350"}).save();
}).then(function () {
return AppFieldsModel.read({id: 1});
}).then(function (updatedAppField) {
should.exist(updatedAppField);
updatedAppField.get("value").should.equal("350");
done();
}).then(null, done);
});
});

View File

@ -0,0 +1,76 @@
/*globals describe, before, beforeEach, afterEach, it*/
var testUtils = require('../../utils'),
should = require('should'),
_ = require("lodash"),
// Stuff we are testing
Models = require('../../../server/models'),
knex = require('../../../server/models/base').knex;
describe('App Setting Model', function () {
var AppSettingModel = Models.AppSetting;
before(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return testUtils.insertAppWithSettings();
})
.then(function () {
done();
}, done);
});
afterEach(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
after(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
it('can browse', function (done) {
AppSettingModel.browse().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
done();
}).then(null, done);
});
it('can read', function (done) {
AppSettingModel.read({id: 1}).then(function (foundAppSetting) {
should.exist(foundAppSetting);
done();
}).then(null, done);
});
it('can edit', function (done) {
AppSettingModel.read({id: 1}).then(function (foundAppSetting) {
should.exist(foundAppSetting);
return foundAppSetting.set({value: "350"}).save();
}).then(function () {
return AppSettingModel.read({id: 1});
}).then(function (updatedAppSetting) {
should.exist(updatedAppSetting);
updatedAppSetting.get("value").should.equal("350");
done();
}).then(null, done);
});
});

View File

@ -0,0 +1,106 @@
/*globals describe, before, beforeEach, afterEach, it*/
var testUtils = require('../../utils'),
should = require('should'),
_ = require("lodash"),
// Stuff we are testing
Models = require('../../../server/models'),
knex = require('../../../server/models/base').knex;
describe('App Model', function () {
var AppModel = Models.App;
before(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
beforeEach(function (done) {
testUtils.initData()
.then(function () {
return testUtils.insertDefaultApp();
})
.then(function () {
done();
}, done);
});
afterEach(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
after(function (done) {
testUtils.clearData().then(function () {
done();
}, done);
});
it('can browse', function (done) {
AppModel.browse().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
done();
}).then(null, done);
});
it('can read', function (done) {
AppModel.read({id: 1}).then(function (foundApp) {
should.exist(foundApp);
done();
}).then(null, done);
});
it('can edit', function (done) {
AppModel.read({id: 1}).then(function (foundApp) {
should.exist(foundApp);
return foundApp.set({name: "New App"}).save();
}).then(function () {
return AppModel.read({id: 1});
}).then(function (updatedApp) {
should.exist(updatedApp);
updatedApp.get("name").should.equal("New App");
done();
}).then(null, done);
});
it("can add", function (done) {
var newApp = testUtils.DataGenerator.forKnex.createApp(testUtils.DataGenerator.Content.apps[1]);
AppModel.add(newApp).then(function (createdApp) {
should.exist(createdApp);
createdApp.attributes.name.should.equal(newApp.name);
done();
}).then(null, done);
});
it("can delete", function (done) {
AppModel.read({id: 1}).then(function (foundApp) {
should.exist(foundApp);
return AppModel['delete'](1);
}).then(function () {
return AppModel.browse();
}).then(function (foundApp) {
var hasRemovedId = foundApp.any(function (foundApp) {
return foundApp.id === 1;
});
hasRemovedId.should.equal(false);
done();
}).then(null, done);
});
});

View File

@ -71,7 +71,7 @@ describe("Permission Model", function () {
action_type: 'test'
};
PermissionModel.add(newPerm).then(function (createdPerm) {
PermissionModel.add(newPerm, {user: 1}).then(function (createdPerm) {
should.exist(createdPerm);
createdPerm.attributes.name.should.equal(newPerm.name);

View File

@ -6,13 +6,12 @@ var testUtils = require('../../utils'),
sequence = require('when/sequence'),
// Stuff we are testing
DataGenerator = require('../../utils/fixtures/data-generator'),
Models = require('../../../server/models');
Models = require('../../../server/models'),
DataGenerator = testUtils.DataGenerator;
describe('Post Model', function () {
var PostModel = Models.Post,
UserModel = Models.User;
var PostModel = Models.Post;
before(function (done) {
testUtils.clearData().then(function () {
@ -66,7 +65,7 @@ describe('Post Model', function () {
}).then(null, done);
});
it('can findAll, returning author and user data', function (done) {
it('can findAll, returning author, user and field data', function (done) {
var firstPost;
PostModel.findAll({}).then(function (results) {
@ -75,13 +74,15 @@ describe('Post Model', function () {
firstPost = results.models[0].toJSON();
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
done();
}, done);
});
it('can findOne, returning author and user data', function (done) {
it('can findOne, returning author, user and field data', function (done) {
var firstPost;
PostModel.findOne({}).then(function (result) {
@ -89,7 +90,9 @@ describe('Post Model', function () {
firstPost = result.toJSON();
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.author.name.should.equal(testUtils.DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
done();
}, done);
@ -117,7 +120,7 @@ describe('Post Model', function () {
newPost = testUtils.DataGenerator.forModel.posts[2],
newPostDB = testUtils.DataGenerator.Content.posts[2];
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
return new PostModel({id: createdPost.id}).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
@ -148,7 +151,7 @@ describe('Post Model', function () {
createdPostUpdatedDate = createdPost.get('updated_at');
// Set the status to published to check that `published_at` is set.
return createdPost.save({status: 'published'});
return createdPost.save({status: 'published'}, {user: 1});
}).then(function (publishedPost) {
publishedPost.get('published_at').should.be.instanceOf(Date);
publishedPost.get('published_by').should.equal(1);
@ -169,7 +172,7 @@ describe('Post Model', function () {
published_at: previousPublishedAtDate,
title: 'published_at test',
markdown: 'This is some content'
}).then(function (newPost) {
}, {user: 1}).then(function (newPost) {
should.exist(newPost);
new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime());
@ -187,7 +190,7 @@ describe('Post Model', function () {
markdown: 'Test Content'
};
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
return new PostModel({ id: createdPost.id }).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
@ -213,7 +216,7 @@ describe('Post Model', function () {
return PostModel.add({
title: 'Test Title',
markdown: 'Test Content ' + (i+1)
});
}, {user: 1});
};
})).then(function (createdPosts) {
// Should have created 12 posts
@ -243,7 +246,7 @@ describe('Post Model', function () {
markdown: 'Test Content 1'
};
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes');
@ -257,7 +260,7 @@ describe('Post Model', function () {
markdown: 'Test Content 1'
};
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
createdPost.get('slug').should.not.equal('rss');
done();
});
@ -269,7 +272,7 @@ describe('Post Model', function () {
markdown: 'Test Content 1'
};
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet');
done();
});
@ -286,13 +289,13 @@ describe('Post Model', function () {
};
// Create the first post
PostModel.add(firstPost)
PostModel.add(firstPost, {user: 1})
.then(function (createdFirstPost) {
// Store the slug for later
firstPost.slug = createdFirstPost.get('slug');
// Create the second post
return PostModel.add(secondPost);
return PostModel.add(secondPost, {user: 1});
}).then(function (createdSecondPost) {
// Store the slug for comparison later
secondPost.slug = createdSecondPost.get('slug');

View File

@ -70,7 +70,7 @@ describe("Role Model", function () {
description: "test1 description"
};
RoleModel.add(newRole).then(function (createdRole) {
RoleModel.add(newRole, {user: 1}).then(function (createdRole) {
should.exist(createdRole);
createdRole.attributes.name.should.equal(newRole.name);

View File

@ -141,7 +141,7 @@ describe('Settings Model', function () {
value: 'Test Content 1'
};
SettingsModel.add(newSetting).then(function (createdSetting) {
SettingsModel.add(newSetting, {user: 1}).then(function (createdSetting) {
should.exist(createdSetting);
createdSetting.has('uuid').should.equal(true);
@ -218,7 +218,7 @@ describe('Settings Model', function () {
});
it('doesn\'t overwrite any existing settings', function (done) {
SettingsModel.edit({key: 'description', value: 'Adam\'s Blog'}).then(function () {
SettingsModel.edit({key: 'description', value: 'Adam\'s Blog'}, {user: 1}).then(function () {
return SettingsModel.populateDefaults();
}).then(function () {
return SettingsModel.read('description');

View File

@ -40,8 +40,8 @@ describe('Tag Model', function () {
createdPostID;
when.all([
PostModel.add(newPost),
TagModel.add(newTag)
PostModel.add(newPost, {user: 1}),
TagModel.add(newTag, {user: 1})
]).then(function (models) {
var createdPost = models[0],
createdTag = models[1];
@ -67,8 +67,8 @@ describe('Tag Model', function () {
createdPostID;
when.all([
PostModel.add(newPost),
TagModel.add(newTag)
PostModel.add(newPost, {user: 1}),
TagModel.add(newTag, {user: 1})
]).then(function (models) {
var createdPost = models[0],
createdTag = models[1];
@ -95,10 +95,10 @@ describe('Tag Model', function () {
function seedTags(tagNames) {
var createOperations = [
PostModel.add(testUtils.DataGenerator.forModel.posts[0])
PostModel.add(testUtils.DataGenerator.forModel.posts[0], {user: 1})
];
var tagModels = tagNames.map(function (tagName) { return TagModel.add({name: tagName}); });
var tagModels = tagNames.map(function (tagName) { return TagModel.add({name: tagName}, {user: 1}); });
createOperations = createOperations.concat(tagModels);
return when.all(createOperations).then(function (models) {
@ -165,7 +165,7 @@ describe('Tag Model', function () {
seedTags(seededTagNames).then(function (_postModel) {
postModel = _postModel;
return TagModel.add({name: 'tag3'});
return TagModel.add({name: 'tag3'}, {user: 1});
}).then(function () {
// the tag API expects tags to be provided like {id: 1, name: 'draft'}
var tagData = seededTagNames.map(function (tagName, i) { return {id: i + 1, name: tagName}; });
@ -198,7 +198,7 @@ describe('Tag Model', function () {
// add the additional tag, and save
tagData.push({id: null, name: 'tag3'});
return postModel.set('tags', tagData).save();
return postModel.set('tags', tagData).save(null, {user: 1});
}).then(function (postModel) {
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
}).then(function (reloadedPost) {
@ -219,7 +219,7 @@ describe('Tag Model', function () {
// add the additional tags, and save
tagData.push({id: null, name: 'tag2'});
tagData.push({id: null, name: 'tag3'});
return postModel.set('tags', tagData).save();
return postModel.set('tags', tagData).save(null, {user: 1});
}).then(function (postModel) {
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
}).then(function (reloadedPost) {
@ -236,7 +236,7 @@ describe('Tag Model', function () {
seedTags(seededTagNames).then(function (_postModel) {
postModel = _postModel;
return TagModel.add({name: 'tag2'});
return TagModel.add({name: 'tag2'}, {user: 1});
}).then(function () {
// the tag API expects tags to be provided like {id: 1, name: 'draft'}
var tagData = seededTagNames.map(function (tagName, i) { return {id: i + 1, name: tagName}; });
@ -247,7 +247,7 @@ describe('Tag Model', function () {
// Add the tag that doesn't exist in the database
tagData.push({id: 3, name: 'tag3'});
return postModel.set('tags', tagData).save();
return postModel.set('tags', tagData).save(null, {user: 1});
}).then(function () {
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
}).then(function (reloadedPost) {
@ -271,7 +271,7 @@ describe('Tag Model', function () {
seedTags(seededTagNames).then(function (_postModel) {
postModel = _postModel;
return TagModel.add({name: 'tag2'});
return TagModel.add({name: 'tag2'}, {user: 1});
}).then(function () {
// the tag API expects tags to be provided like {id: 1, name: 'draft'}
var tagData = seededTagNames.map(function (tagName, i) { return {id: i + 1, name: tagName}; });
@ -283,7 +283,7 @@ describe('Tag Model', function () {
tagData.push({id: 3, name: 'tag3'});
tagData.push({id: 4, name: 'tag4'});
return postModel.set('tags', tagData).save();
return postModel.set('tags', tagData).save(null, {user: 1});
}).then(function () {
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
}).then(function (reloadedPost) {
@ -304,7 +304,7 @@ describe('Tag Model', function () {
it('can add a tag to a post on creation', function (done) {
var newPost = _.extend(testUtils.DataGenerator.forModel.posts[0], {tags: [{name: 'test_tag_1'}]})
PostModel.add(newPost).then(function (createdPost) {
PostModel.add(newPost, {user: 1}).then(function (createdPost) {
return PostModel.read({id: createdPost.id, status: 'all'}, { withRelated: ['tags']});
}).then(function (postWithTag) {
postWithTag.related('tags').length.should.equal(1);

View File

@ -39,7 +39,7 @@ describe('User Model', function run() {
return when.resolve(userData);
});
UserModel.add(userData).then(function (createdUser) {
UserModel.add(userData, {user: 1}).then(function (createdUser) {
should.exist(createdUser);
createdUser.has('uuid').should.equal(true);
createdUser.attributes.password.should.not.equal(userData.password, "password was hashed");
@ -55,7 +55,7 @@ describe('User Model', function run() {
return when.resolve(userData);
});
UserModel.add(userData).then(function (createdUser) {
UserModel.add(userData, {user: 1}).then(function (createdUser) {
should.exist(createdUser);
createdUser.has('uuid').should.equal(true);
createdUser.attributes.email.should.eql(userData.email, "email address correct");
@ -67,11 +67,11 @@ describe('User Model', function run() {
it('can find gravatar', function (done) {
var userData = testUtils.DataGenerator.forModel.users[4],
gravatarStub = sinon.stub(UserModel, 'gravatarLookup', function (userData) {
userData.image = 'http://www.gravatar.com/avatar/2fab21a4c4ed88e76add10650c73bae1?d=404'
userData.image = 'http://www.gravatar.com/avatar/2fab21a4c4ed88e76add10650c73bae1?d=404';
return when.resolve(userData);
});
UserModel.add(userData).then(function (createdUser) {
UserModel.add(userData, {user: 1}).then(function (createdUser) {
should.exist(createdUser);
createdUser.has('uuid').should.equal(true);
createdUser.attributes.image.should.eql('http://www.gravatar.com/avatar/2fab21a4c4ed88e76add10650c73bae1?d=404', 'Gravatar found');
@ -86,7 +86,7 @@ describe('User Model', function run() {
return when.resolve(userData);
});
UserModel.add(userData).then(function (createdUser) {
UserModel.add(userData, {user: 1}).then(function (createdUser) {
should.exist(createdUser);
createdUser.has('uuid').should.equal(true);
should.not.exist(createdUser.image);
@ -99,7 +99,7 @@ describe('User Model', function run() {
var userData = testUtils.DataGenerator.forModel.users[2],
email = testUtils.DataGenerator.forModel.users[2].email;
UserModel.add(userData).then(function () {
UserModel.add(userData, {user: 1}).then(function () {
// Test same case
return UserModel.getByEmail(email).then(function (user) {
should.exist(user);
@ -153,7 +153,7 @@ describe('User Model', function run() {
it('can\'t add second', function (done) {
var userData = testUtils.DataGenerator.forModel.users[1];
return UserModel.add(userData).then(done, function (failure) {
return UserModel.add(userData, {user: 1}).then(done, function (failure) {
failure.message.should.eql('A user is already registered. Only one user for now!');
done();
}).then(null, done);
@ -220,16 +220,6 @@ describe('User Model', function run() {
}).then(null, done);
});
it("can get effective permissions", function (done) {
UserModel.effectivePermissions(1).then(function (effectivePermissions) {
should.exist(effectivePermissions);
effectivePermissions.length.should.be.above(0);
done();
}).then(null, done);
});
it('can delete', function (done) {
var firstUserId;

View File

@ -5,13 +5,15 @@ var fs = require('fs'),
should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
when = require('when'),
helpers = require('../../server/helpers'),
filters = require('../../server/filters'),
// Stuff we are testing
appProxy = require('../../server/apps/proxy'),
AppSandbox = require('../../server/apps/sandbox'),
AppDependencies = require('../../server/apps/dependencies');
AppDependencies = require('../../server/apps/dependencies'),
AppPermissions = require('../../server/apps/permissions');
describe('Apps', function () {
@ -189,4 +191,127 @@ describe('Apps', function () {
});
});
});
describe('Permissions', function () {
var noGhostPackageJson = {
"name": "myapp",
"version": "0.0.1",
"description": "My example app",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ghost",
"license": "MIT",
"dependencies": {
"ghost-app": "0.0.1"
}
},
validGhostPackageJson = {
"name": "myapp",
"version": "0.0.1",
"description": "My example app",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ghost",
"license": "MIT",
"dependencies": {
"ghost-app": "0.0.1"
},
"ghost": {
"permissions": {
"posts": ["browse", "read", "edit", "add", "delete"],
"users": ["browse", "read", "edit", "add", "delete"],
"settings": ["browse", "read", "edit", "add", "delete"]
}
}
};
it('has default permissions to read and browse posts', function () {
should.exist(AppPermissions.DefaultPermissions);
should.exist(AppPermissions.DefaultPermissions.posts);
AppPermissions.DefaultPermissions.posts.should.contain('browse');
AppPermissions.DefaultPermissions.posts.should.contain('read');
// Make it hurt to add more so additional checks are added here
_.keys(AppPermissions.DefaultPermissions).length.should.equal(1);
});
it('uses default permissions if no package.json', function (done) {
var perms = new AppPermissions("test");
// No package.json in this directory
sandbox.stub(perms, "checkPackageContentsExists").returns(when.resolve(false));
perms.read().then(function (readPerms) {
should.exist(readPerms);
readPerms.should.equal(AppPermissions.DefaultPermissions);
done();
}).otherwise(done);
});
it('uses default permissions if no ghost object in package.json', function (done) {
var perms = new AppPermissions("test"),
noGhostPackageJsonContents = JSON.stringify(noGhostPackageJson, null, 2);
// package.json IS in this directory
sandbox.stub(perms, "checkPackageContentsExists").returns(when.resolve(true));
// no ghost property on package
sandbox.stub(perms, "getPackageContents").returns(when.resolve(noGhostPackageJsonContents));
perms.read().then(function (readPerms) {
should.exist(readPerms);
readPerms.should.equal(AppPermissions.DefaultPermissions);
done();
}).otherwise(done);
});
it('rejects when reading malformed package.json', function (done) {
var perms = new AppPermissions("test");
// package.json IS in this directory
sandbox.stub(perms, "checkPackageContentsExists").returns(when.resolve(true));
// malformed JSON on package
sandbox.stub(perms, "getPackageContents").returns(when.reject(new Error('package.json file is malformed')));
perms.read().then(function (readPerms) {
done(new Error('should not resolve'));
}).otherwise(function () {
done();
});
});
it('reads from package.json in root of app directory', function (done) {
var perms = new AppPermissions("test"),
validGhostPackageJsonContents = validGhostPackageJson;
// package.json IS in this directory
sandbox.stub(perms, "checkPackageContentsExists").returns(when.resolve(true));
// valid ghost property on package
sandbox.stub(perms, "getPackageContents").returns(when.resolve(validGhostPackageJsonContents));
perms.read().then(function (readPerms) {
should.exist(readPerms);
readPerms.should.not.equal(AppPermissions.DefaultPermissions);
should.exist(readPerms.posts);
readPerms.posts.length.should.equal(5);
should.exist(readPerms.users);
readPerms.users.length.should.equal(5);
should.exist(readPerms.settings);
readPerms.settings.length.should.equal(5);
_.keys(readPerms).length.should.equal(3);
done();
}).otherwise(done);
});
});
});

View File

@ -15,6 +15,8 @@ describe("Exporter", function () {
should.exist(exporter);
var sandbox;
before(function (done) {
testUtils.clearData().then(function () {
done();
@ -22,12 +24,14 @@ describe("Exporter", function () {
});
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData().then(function () {
done();
}, done);
});
afterEach(function (done) {
sandbox.restore();
testUtils.clearData().then(function () {
done();
}, done);
@ -35,8 +39,8 @@ describe("Exporter", function () {
it("exports data", function (done) {
// Stub migrations to return 000 as the current database version
var migrationStub = sinon.stub(migration, "getDatabaseVersion", function () {
return when.resolve("002");
var migrationStub = sandbox.stub(migration, "getDatabaseVersion", function () {
return when.resolve("003");
});
exporter().then(function (exportData) {
@ -48,8 +52,8 @@ describe("Exporter", function () {
should.exist(exportData.meta);
should.exist(exportData.data);
exportData.meta.version.should.equal("002");
_.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("002");
exportData.meta.version.should.equal("003");
_.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("003");
_.each(tables, function (name) {
should.exist(exportData.data[name]);

View File

@ -1,37 +1,45 @@
/*globals describe, beforeEach, it*/
var testUtils = require('../utils'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
assert = require('assert'),
_ = require("lodash"),
errors = require('../../server/errorHandling'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
assert = require('assert'),
_ = require("lodash"),
errors = require('../../server/errorHandling'),
// Stuff we are testing
knex = require("../../server/models/base").knex,
migration = require('../../server/data/migration'),
exporter = require('../../server/data/export'),
importer = require('../../server/data/import'),
knex = require("../../server/models/base").knex,
migration = require('../../server/data/migration'),
exporter = require('../../server/data/export'),
importer = require('../../server/data/import'),
Importer000 = require('../../server/data/import/000'),
Importer001 = require('../../server/data/import/001'),
Importer002 = require('../../server/data/import/002'),
fixtures = require('../../server/data/fixtures'),
Settings = require('../../server/models/settings').Settings;
Importer003 = require('../../server/data/import/003'),
fixtures = require('../../server/data/fixtures'),
Settings = require('../../server/models/settings').Settings;
describe("Import", function () {
should.exist(exporter);
should.exist(importer);
var sandbox;
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
// clear database... we need to initialise it manually for each test
testUtils.clearData().then(function () {
done();
}, done);
});
afterEach(function () {
sandbox.restore();
});
it("resolves 000", function (done) {
var importStub = sinon.stub(Importer000, "importData", function () {
var importStub = sandbox.stub(Importer000, "importData", function () {
return when.resolve();
}),
fakeData = { test: true };
@ -46,7 +54,7 @@ describe("Import", function () {
});
it("resolves 001", function (done) {
var importStub = sinon.stub(Importer001, "importData", function () {
var importStub = sandbox.stub(Importer001, "importData", function () {
return when.resolve();
}),
fakeData = { test: true };
@ -61,7 +69,7 @@ describe("Import", function () {
});
it("resolves 002", function (done) {
var importStub = sinon.stub(Importer002, "importData", function () {
var importStub = sandbox.stub(Importer002, "importData", function () {
return when.resolve();
}),
fakeData = { test: true };
@ -75,18 +83,27 @@ describe("Import", function () {
}).then(null, done);
});
it("resolves 003", function (done) {
var importStub = sandbox.stub(Importer003, "importData", function () {
return when.resolve();
}),
fakeData = { test: true };
importer("003", fakeData).then(function () {
importStub.calledWith(fakeData).should.equal(true);
importStub.restore();
done();
}).then(null, done);
});
describe("000", function () {
should.exist(Importer000);
beforeEach(function (done) {
// migrate to current version
migration.migrateUp().then(function () {
// Load the fixtures
return fixtures.populateFixtures();
}).then(function () {
// Initialise the default settings
return Settings.populateDefaults();
}).then(function () {
migration.migrateUpFreshDb().then(function () {
return testUtils.insertDefaultUser();
}).then(function () {
done();
@ -96,7 +113,7 @@ describe("Import", function () {
it("imports data from 000", function (done) {
var exportData,
migrationStub = sinon.stub(migration, "getDatabaseVersion", function () {
migrationStub = sandbox.stub(migration, "getDatabaseVersion", function () {
return when.resolve("000");
});
@ -129,7 +146,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// test tags
tags.length.should.equal(exportData.data.tags.length, 'no new tags');
@ -146,13 +163,7 @@ describe("Import", function () {
beforeEach(function (done) {
// migrate to current version
migration.migrateUp().then(function () {
// Load the fixtures
return fixtures.populateFixtures();
}).then(function () {
// Initialise the default settings
return Settings.populateDefaults();
}).then(function () {
migration.migrateUpFreshDb().then(function () {
return testUtils.insertDefaultUser();
}).then(function () {
done();
@ -207,7 +218,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// activeTheme should NOT have been overridden
_.findWhere(settings, {key: "activeTheme"}).value.should.equal("casper", 'Wrong theme');
@ -270,7 +281,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// test tags
tags.length.should.equal(exportData.data.tags.length, 'no new tags');
@ -316,7 +327,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// test tags
tags.length.should.equal(exportData.data.tags.length, 'no new tags');
@ -329,17 +340,11 @@ describe("Import", function () {
});
describe("002", function () {
should.exist(Importer001);
should.exist(Importer002);
beforeEach(function (done) {
// migrate to current version
migration.migrateUp().then(function () {
// Load the fixtures
return fixtures.populateFixtures();
}).then(function () {
// Initialise the default settings
return Settings.populateDefaults();
}).then(function () {
migration.migrateUpFreshDb().then(function () {
return testUtils.insertDefaultUser();
}).then(function () {
done();
@ -394,7 +399,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// activeTheme should NOT have been overridden
_.findWhere(settings, {key: "activeTheme"}).value.should.equal("casper", 'Wrong theme');
@ -416,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) {
@ -457,7 +464,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// test tags
tags.length.should.equal(exportData.data.tags.length, 'no new tags');
@ -503,7 +510,7 @@ describe("Import", function () {
// test settings
settings.length.should.be.above(0, 'Wrong number of settings');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("002", 'Wrong database version');
_.findWhere(settings, {key: "databaseVersion"}).value.should.equal("003", 'Wrong database version');
// test tags
tags.length.should.equal(exportData.data.tags.length, 'no new tags');
@ -514,4 +521,47 @@ describe("Import", function () {
}).then(null, done);
});
});
describe("003", function () {
should.exist(Importer003);
beforeEach(function (done) {
// migrate to current version
migration.migrateUpFreshDb().then(function () {
return testUtils.insertDefaultUser();
}).then(function () {
done();
}).then(null, done);
});
it("safely imports data from 003", function (done) {
var exportData;
testUtils.loadExportFixture('export-003').then(function (exported) {
exportData = exported;
return importer("003", exportData);
}).then(function () {
// Grab the data from tables
return when.all([
knex("apps").select(),
knex("app_settings").select()
]);
}).then(function (importedData) {
should.exist(importedData);
importedData.length.should.equal(2, 'Did not get data successfully');
var apps = importedData[0],
app_settings = importedData[1];
// test apps
apps.length.should.equal(exportData.data.apps.length, 'imported apps');
// test app settings
// app_settings.length.should.equal(exportData.data.app_settings.length, 'imported app settings');
done();
}).then(null, done);
});
});
});

View File

@ -8,6 +8,7 @@ var testUtils = require('../utils'),
// Stuff we are testing
permissions = require('../../server/permissions'),
effectivePerms = require('../../server/permissions/effective'),
Models = require('../../server/models'),
UserProvider = Models.User,
PermissionsProvider = Models.Permission,
@ -15,6 +16,8 @@ var testUtils = require('../utils'),
describe('Permissions', function () {
var sandbox;
before(function (done) {
testUtils.clearData().then(function () {
done();
@ -22,13 +25,17 @@ describe('Permissions', function () {
});
beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData()
.then(testUtils.insertDefaultUser).then(function () {
.then(testUtils.insertDefaultUser)
.then(testUtils.insertDefaultApp)
.then(function () {
done();
}, done);
});
afterEach(function (done) {
sandbox.restore();
testUtils.clearData()
.then(function () {
done();
@ -80,7 +87,7 @@ describe('Permissions', function () {
object_type: obj
};
return PermissionsProvider.add(newPerm);
return PermissionsProvider.add(newPerm, {user: 1});
},
createTestPermissions = function () {
var createActions = _.map(testPerms, function (testPerm) {
@ -97,7 +104,7 @@ describe('Permissions', function () {
.then(function (actionsMap) {
should.exist(actionsMap);
actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page'].sort());
actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page', 'setting'].sort());
actionsMap.should.equal(permissions.actionsMap);
@ -120,7 +127,7 @@ describe('Permissions', function () {
existingUserRoles = foundUser.related('roles').length;
return testRole.save().then(function () {
return testRole.save(null, {user: 1}).then(function () {
return foundUser.roles().attach(testRole);
});
}).then(function () {
@ -144,7 +151,7 @@ describe('Permissions', function () {
testUser.related('permissions').length.should.equal(0);
return testPermission.save().then(function () {
return testPermission.save(null, {user: 1}).then(function () {
return testUser.permissions().attach(testPermission);
});
}).then(function () {
@ -164,7 +171,7 @@ describe('Permissions', function () {
description: "test2 description"
});
testRole.save()
testRole.save(null, {user: 1})
.then(function () {
return testRole.load('permissions');
})
@ -177,7 +184,7 @@ describe('Permissions', function () {
testRole.related('permissions').length.should.equal(0);
return rolePermission.save().then(function () {
return rolePermission.save(null, {user: 1}).then(function () {
return testRole.permissions().attach(rolePermission);
});
})
@ -233,7 +240,7 @@ describe('Permissions', function () {
object_type: "post"
});
return newPerm.save().then(function () {
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm);
});
})
@ -243,7 +250,6 @@ describe('Permissions', function () {
.then(function (updatedUser) {
// TODO: Verify updatedUser.related('permissions') has the permission?
var canThisResult = permissions.canThis(updatedUser.id);
should.exist(canThisResult.edit);
@ -258,21 +264,22 @@ describe('Permissions', function () {
it('can use permissable function on Model to allow something', function (done) {
var testUser,
permissableStub = sinon.stub(PostProvider, 'permissable', function () {
permissableStub = sandbox.stub(PostProvider, 'permissable', 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();
})
@ -286,29 +293,151 @@ describe('Permissions', function () {
it('can use permissable function on Model to forbid something', function (done) {
var testUser,
permissableStub = sinon.stub(PostProvider, 'permissable', function () {
permissableStub = sandbox.stub(PostProvider, 'permissable', 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);
})
.then(function () {
permissableStub.restore();
errors.logError(new Error("Allowed testUser to edit post"));
done(new Error("Allowed testUser to edit post"));
})
.otherwise(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();
});
});
it("can get effective user permissions", function (done) {
effectivePerms.user(1).then(function (effectivePermissions) {
should.exist(effectivePermissions);
effectivePermissions.length.should.be.above(0);
done();
}).then(null, done);
});
it('can check an apps effective permissions', function (done) {
effectivePerms.app('Kudos')
.then(function (effectivePermissions) {
should.exist(effectivePermissions);
effectivePermissions.length.should.be.above(0);
done();
})
.otherwise(done);
});
it('does not allow an app to edit a post without permission', function (done) {
// Change the author of the post so the author override doesn't affect the test
PostProvider.edit({id: 1, 'author_id': 2})
.then(function (updatedPost) {
// Add user permissions
return Models.User.read({id: 1})
.then(function (foundUser) {
var newPerm = new Models.Permission({
name: "app test edit post",
action_type: "edit",
object_type: "post"
});
return newPerm.save(null, {user: 1}).then(function () {
return foundUser.permissions().attach(newPerm).then(function () {
return when.all([updatedPost, foundUser]);
});
});
});
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
return permissions.canThis({ user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
return results;
})
.otherwise(function (err) {
done(new Error("Did not allow user 1 to edit post 1"));
});
})
.then(function (results) {
var updatedPost = results[0],
updatedUser = results[1];
// Confirm app cannot edit it.
return permissions.canThis({ app: 'Hemingway', user: updatedUser.id })
.edit
.post(updatedPost.id)
.then(function () {
done(new Error("Allowed an edit of post 1"));
})
.otherwise(function () {
done();
});
}).otherwise(done);
});
it('allows an app to edit a post with permission', function (done) {
permissions.canThis({ app: 'Kudos', user: 1 })
.edit
.post(1)
.then(function () {
done();
})
.otherwise(function () {
done(new Error("Allowed an edit of post 1"));
});
});
it('checks for null context passed and rejects', function (done) {
permissions.canThis(undefined)
.edit
.post(1)
.then(function () {
done(new Error("Should not allow editing post"));
})
.otherwise(function () {
done();
});
});
it('allows \'internal\' to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis('internal')
.edit
.tag(1)
.then(function () {
done();
})
.otherwise(function () {
done(new Error("Should allow editing post with 'internal'"));
});
});
it('allows { internal: true } to be passed for internal requests', function (done) {
// Using tag here because post implements the custom permissable interface
permissions.canThis({ internal: true })
.edit
.tag(1)
.then(function () {
done();
})
.otherwise(function () {
done(new Error("Should allow editing post with { internal: true }"));
});
});
});

View File

@ -8,7 +8,7 @@ var _ = require('lodash'),
posts: ['posts', 'page', 'limit', 'pages', 'total'],
post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at',
'updated_by', 'published_at', 'published_by', 'page', 'author', 'tags'],
'updated_by', 'published_at', 'published_by', 'page', 'author', 'tags', 'fields'],
// TODO: remove databaseVersion, dbHash
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang',
"permalinks", 'postsPerPage', 'forceI18n', 'activeTheme', 'activeApps', 'installedApps',

View File

@ -101,6 +101,51 @@ DataGenerator.Content = {
email: 'info@ghost.org',
password: '$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKZL6'
}
],
apps: [
{
name: 'Kudos',
slug: 'kudos',
version: '0.0.1',
status: 'installed'
},
{
name: 'Importer',
slug: 'importer',
version: '0.1.0',
status: 'inactive'
},
{
name: 'Hemingway',
slug: 'hemingway',
version: '1.0.0',
status: 'installed'
}
],
app_fields: [
{
key: 'count',
value: '120',
type: 'number'
},
{
key: 'words',
value: '512',
type: 'number'
}
],
app_settings: [
{
key: 'color',
value: 'ghosty'
},
{
key: 'setting',
value: 'value'
}
]
};
@ -108,7 +153,9 @@ DataGenerator.forKnex = (function () {
var posts,
tags,
posts_tags;
posts_tags,
apps,
app_fields;
function createPost(overrides) {
return _.defaults(overrides, {
@ -185,6 +232,33 @@ DataGenerator.forKnex = (function () {
};
}
function createApp(overrides) {
return _.defaults(overrides, {
uuid: uuid.v4(),
created_by: 1,
created_at: new Date()
});
}
function createAppField(overrides) {
return _.defaults(overrides, {
uuid: uuid.v4(),
created_by: 1,
created_at: new Date(),
app_id: 1,
relatable_id: 1,
relatable_type: 'posts'
});
}
function createAppSetting(overrides) {
return _.defaults(overrides, {
uuid: uuid.v4(),
created_by: 1,
created_at: new Date()
});
}
posts = [
createPost(DataGenerator.Content.posts[0]),
createPost(DataGenerator.Content.posts[1]),
@ -212,6 +286,17 @@ DataGenerator.forKnex = (function () {
{ post_id: 5, tag_id: 5 }
];
apps = [
createApp(DataGenerator.Content.apps[0]),
createApp(DataGenerator.Content.apps[1]),
createApp(DataGenerator.Content.apps[2])
];
app_fields = [
createAppField(DataGenerator.Content.app_fields[0]),
createAppField(DataGenerator.Content.app_fields[1])
];
return {
createPost: createPost,
createGenericPost: createGenericPost,
@ -220,10 +305,15 @@ DataGenerator.forKnex = (function () {
createGenericUser: createGenericUser,
createUserRole: createUserRole,
createPostsTags: createPostsTags,
createApp: createApp,
createAppField: createAppField,
createAppSetting: createAppSetting,
posts: posts,
tags: tags,
posts_tags: posts_tags
posts_tags: posts_tags,
apps: apps,
app_fields: app_fields
};
}());

View File

@ -0,0 +1,369 @@
{
"meta": {
"exported_on": 1388318311015,
"version": "003"
},
"data": {
"posts": [
{
"id": 1,
"uuid": "8492fbba-1102-4b53-8e3e-abe207952f0c",
"title": "Welcome to Ghost",
"slug": "welcome-to-ghost",
"markdown": "You're live! Nice.",
"html": "<p>You're live! Nice.</p>",
"image": null,
"featured": 0,
"page": 0,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"author_id": 1,
"created_at": 1388318310782,
"created_by": 1,
"updated_at": 1388318310782,
"updated_by": 1,
"published_at": 1388318310783,
"published_by": 1
}
],
"users": [
{
"id": 1,
"uuid": "e5188224-4742-4c32-a2d6-e9c5c5d4c123",
"name": "Josephine Bloggs",
"slug": "josephine-blogs",
"password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC",
"email": "josephinebloggs@example.com",
"image": null,
"cover": null,
"bio": "A blogger",
"website": null,
"location": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"last_login": null,
"created_at": 1388319501897,
"created_by": 1,
"updated_at": null,
"updated_by": null
}
],
"roles": [
{
"id": 1,
"uuid": "d2ea9c7f-7e6b-4cae-b009-35c298206852",
"name": "Administrator",
"description": "Administrators",
"created_at": 1388318310794,
"created_by": 1,
"updated_at": 1388318310794,
"updated_by": 1
},
{
"id": 2,
"uuid": "b0d7d6b0-5b88-45b5-b0e5-a487741b843d",
"name": "Editor",
"description": "Editors",
"created_at": 1388318310796,
"created_by": 1,
"updated_at": 1388318310796,
"updated_by": 1
},
{
"id": 3,
"uuid": "9f72e817-5490-4ccf-bc78-c557dc9613ca",
"name": "Author",
"description": "Authors",
"created_at": 1388318310799,
"created_by": 1,
"updated_at": 1388318310799,
"updated_by": 1
}
],
"roles_users": [
{
"id": 1,
"role_id": 1,
"user_id": 1
}
],
"permissions": [
{
"id": 1,
"uuid": "bdfbd261-e0fb-4c8e-abab-aece7a9e8e34",
"name": "Edit posts",
"object_type": "post",
"action_type": "edit",
"object_id": null,
"created_at": 1388318310803,
"created_by": 1,
"updated_at": 1388318310803,
"updated_by": 1
},
{
"id": 2,
"uuid": "580d31c4-e3db-40f3-969d-9a1caea9d1bb",
"name": "Remove posts",
"object_type": "post",
"action_type": "remove",
"object_id": null,
"created_at": 1388318310814,
"created_by": 1,
"updated_at": 1388318310814,
"updated_by": 1
},
{
"id": 3,
"uuid": "c1f8b024-e383-494a-835d-6fb673f143db",
"name": "Create posts",
"object_type": "post",
"action_type": "create",
"object_id": null,
"created_at": 1388318310818,
"created_by": 1,
"updated_at": 1388318310818,
"updated_by": 1
}
],
"permissions_users": [],
"permissions_roles": [
{
"id": 1,
"role_id": 1,
"permission_id": 1
},
{
"id": 2,
"role_id": 1,
"permission_id": 2
},
{
"id": 3,
"role_id": 1,
"permission_id": 3
}
],
"settings": [
{
"id": 1,
"uuid": "f90aa810-4fa2-49fe-a39b-7c0d2ebb473e",
"key": "databaseVersion",
"value": "001",
"type": "core",
"created_at": 1388318310829,
"created_by": 1,
"updated_at": 1388318310829,
"updated_by": 1
},
{
"id": 2,
"uuid": "95ce1c53-69b0-4f5f-be91-d3aeb39046b5",
"key": "dbHash",
"value": null,
"type": "core",
"created_at": 1388318310829,
"created_by": 1,
"updated_at": 1388318310829,
"updated_by": 1
},
{
"id": 3,
"uuid": "c356fbde-0bc5-4fe1-9309-2510291aa34d",
"key": "title",
"value": "Ghost",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 4,
"uuid": "858dc11f-8f9e-4011-99ee-d94c48d5a2ce",
"key": "description",
"value": "Just a blogging platform.",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 5,
"uuid": "37ca5ae7-bca6-4dd5-8021-4ef6c6dcb097",
"key": "email",
"value": "josephinebloggs@example.com",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 6,
"uuid": "1672d62c-fab7-4f22-b333-8cf760189f67",
"key": "logo",
"value": "",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 7,
"uuid": "cd8b0456-578b-467a-857e-551bad17a14d",
"key": "cover",
"value": "",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 8,
"uuid": "c4a074a4-05c7-49f7-83eb-068302c15d82",
"key": "defaultLang",
"value": "en_US",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 9,
"uuid": "21f2f5da-9bee-4dae-b3b7-b8d7baf8be33",
"key": "postsPerPage",
"value": "6",
"type": "blog",
"created_at": 1388318310830,
"created_by": 1,
"updated_at": 1388318310830,
"updated_by": 1
},
{
"id": 10,
"uuid": "2d21b736-f85a-4119-a0e3-5fc898b1bf47",
"key": "forceI18n",
"value": "true",
"type": "blog",
"created_at": 1388318310831,
"created_by": 1,
"updated_at": 1388318310831,
"updated_by": 1
},
{
"id": 11,
"uuid": "5c5b91b8-6062-4104-b855-9e121f72b0f0",
"key": "permalinks",
"value": "/:slug/",
"type": "blog",
"created_at": 1388318310831,
"created_by": 1,
"updated_at": 1388318310831,
"updated_by": 1
},
{
"id": 12,
"uuid": "795cb328-3e38-4906-81a8-fcdff19d914f",
"key": "activeTheme",
"value": "notcasper",
"type": "theme",
"created_at": 1388318310831,
"created_by": 1,
"updated_at": 1388318310831,
"updated_by": 1
},
{
"id": 13,
"uuid": "f3afce35-5166-453e-86c3-50dfff74dca7",
"key": "activeApps",
"value": "[]",
"type": "plugin",
"created_at": 1388318310831,
"created_by": 1,
"updated_at": 1388318310831,
"updated_by": 1
},
{
"id": 14,
"uuid": "2ea560a3-2304-449d-a62b-f7b622987510",
"key": "installedApps",
"value": "[]",
"type": "plugin",
"created_at": 1388318310831,
"created_by": 1,
"updated_at": 1388318310831,
"updated_by": 1
}
],
"tags": [
{
"id": 1,
"uuid": "a950117a-9735-4584-931d-25a28015a80d",
"name": "Getting Started",
"slug": "getting-started",
"description": null,
"parent_id": null,
"meta_title": null,
"meta_description": null,
"created_at": 1388318310790,
"created_by": 1,
"updated_at": 1388318310790,
"updated_by": 1
}
],
"posts_tags": [
{
"id": 1,
"post_id": 1,
"tag_id": 1
}
],
"apps": [
{
"id": 1,
"uuid": "4d7557f0-0949-4946-9fe8-ec030e0727f0",
"name": "Kudos",
"slug": "kudos",
"version": "0.0.1",
"status": "installed",
"created_at": 1388318312790,
"created_by": 1,
"updated_at": 1388318312790,
"updated_by": 1
}
],
"app_settings": [
{
"id": 1,
"uuid": "790e4551-b9cc-4954-8f5d-b6e651bc7342",
"key": "position",
"value": "bottom",
"app_id": 1,
"created_at": 1388318312790,
"created_by": 1,
"updated_at": 1388318312790,
"updated_by": 1
},
{
"id": 2,
"uuid": "29682b66-cdeb-4773-9821-bcf40ea93b58",
"key": "size",
"value": "60",
"app_id": 1,
"created_at": 1388318312790,
"created_by": 1,
"updated_at": 1388318312790,
"updated_by": 1
}
]
}
}

View File

@ -20,6 +20,7 @@ function clearData() {
}
function insertPosts() {
// ToDo: Get rid of pyramid of doom
return when(knex('posts').insert(DataGenerator.forKnex.posts).then(function () {
return knex('tags').insert(DataGenerator.forKnex.tags).then(function () {
return knex('posts_tags').insert(DataGenerator.forKnex.posts_tags);
@ -91,15 +92,105 @@ function insertDefaultUser() {
users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[0]));
userRoles.push(DataGenerator.forKnex.createUserRole(1, 1));
return when(knex('users').insert(users).then(function () {
return knex('roles_users').insert(userRoles);
}));
return knex('users')
.insert(users)
.then(function () {
return knex('roles_users').insert(userRoles);
});
}
function insertEditorUser() {
var users = [],
userRoles = [];
users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[1]));
userRoles.push(DataGenerator.forKnex.createUserRole(2, 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(3, 3));
return knex('users')
.insert(users)
.then(function () {
return knex('roles_users').insert(userRoles);
});
}
function insertDefaultApp() {
var apps = [];
apps.push(DataGenerator.forKnex.createApp(DataGenerator.Content.apps[0]));
return knex('apps')
.insert(apps)
.then(function () {
return knex('permissions_apps')
.insert({
app_id: 1,
permission_id: 1
});
});
}
function insertApps() {
return knex('apps').insert(DataGenerator.forKnex.apps).then(function () {
return knex('app_fields').insert(DataGenerator.forKnex.app_fields);
});
}
function insertAppWithSettings() {
var apps = [], app_settings = [];
apps.push(DataGenerator.forKnex.createApp(DataGenerator.Content.apps[0]));
app_settings.push(DataGenerator.forKnex.createAppSetting(DataGenerator.Content.app_settings[0]));
app_settings.push(DataGenerator.forKnex.createAppSetting(DataGenerator.Content.app_settings[1]));
return knex('apps').insert(apps, 'id')
.then(function (results) {
var appId = results[0];
for (var i = 0; i < app_settings.length; i++) {
app_settings[i].app_id = appId;
}
return knex('app_settings').insert(app_settings);
});
}
function insertAppWithFields() {
var apps = [], app_fields = [];
apps.push(DataGenerator.forKnex.createApp(DataGenerator.Content.apps[0]));
app_fields.push(DataGenerator.forKnex.createAppField(DataGenerator.Content.app_fields[0]));
app_fields.push(DataGenerator.forKnex.createAppField(DataGenerator.Content.app_fields[1]));
return knex('apps').insert(apps, 'id')
.then(function (results) {
var appId = results[0];
for (var i = 0; i < app_fields.length; i++) {
app_fields[i].app_id = appId;
}
return knex('app_fields').insert(app_fields);
});
}
function insertDefaultFixtures() {
return when(insertDefaultUser().then(function () {
return insertPosts();
}));
return insertDefaultUser().then(function () {
return insertPosts()
}).then(function () {
return insertApps();
});
}
function loadExportFixture(filename) {
@ -127,6 +218,12 @@ module.exports = {
insertMorePosts: insertMorePosts,
insertMorePostsTags: insertMorePostsTags,
insertDefaultUser: insertDefaultUser,
insertEditorUser: insertEditorUser,
insertAuthorUser: insertAuthorUser,
insertDefaultApp: insertDefaultApp,
insertApps: insertApps,
insertAppWithSettings: insertAppWithSettings,
insertAppWithFields: insertAppWithFields,
loadExportFixture: loadExportFixture,

View File

@ -32,7 +32,7 @@
"engineStrict": true,
"dependencies": {
"bcryptjs": "0.7.10",
"bookshelf": "0.6.1",
"bookshelf": "0.6.8",
"busboy": "0.0.12",
"colors": "0.6.2",
"connect-slashes": "1.2.0",