From 13d2d04c7209652728773bcaaf2369aa691cd003 Mon Sep 17 00:00:00 2001 From: Jacob Gable Date: Tue, 11 Feb 2014 19:04:13 -0600 Subject: [PATCH] App Permissions from package.json Progress on #2095 - Add new AppPermissions class with read() method - has default permissions to read and browse posts - uses default permissions if no package.json - uses default permissions if no ghost object in package.json - errors when reading malformed package.json - uses ghost.permissions if found in package.json --- core/server/apps/loader.js | 61 +++++++++------ core/server/apps/permissions.js | 75 +++++++++++++++++++ core/test/unit/apps_spec.js | 127 +++++++++++++++++++++++++++++++- 3 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 core/server/apps/permissions.js diff --git a/core/server/apps/loader.js b/core/server/apps/loader.js index 17148cd152..8e7aac5dfe 100644 --- a/core/server/apps/loader.js +++ b/core/server/apps/loader.js @@ -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); + }); }); } }; diff --git a/core/server/apps/permissions.js b/core/server/apps/permissions.js new file mode 100644 index 0000000000..25202d6144 --- /dev/null +++ b/core/server/apps/permissions.js @@ -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; \ No newline at end of file diff --git a/core/test/unit/apps_spec.js b/core/test/unit/apps_spec.js index 9a60579918..defd2d3510 100644 --- a/core/test/unit/apps_spec.js +++ b/core/test/unit/apps_spec.js @@ -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); + }); + }); });