diff --git a/core/server/apps/loader.js b/core/server/apps/loader.js index 8e7aac5dfe..d84c16cc63 100644 --- a/core/server/apps/loader.js +++ b/core/server/apps/loader.js @@ -2,7 +2,7 @@ var path = require('path'), _ = require('lodash'), when = require('when'), - appProxy = require('./proxy'), + AppProxy = require('./proxy'), config = require('../config'), AppSandbox = require('./sandbox'), AppDependencies = require('./dependencies'), @@ -29,9 +29,13 @@ function loadApp(appPath) { return sandbox.loadApp(appPath); } -function getAppByName(name) { +function getAppByName(name, permissions) { // Grab the app class to instantiate var AppClass = loadApp(getAppRelativePath(name)), + appProxy = new AppProxy({ + name: name, + permissions: permissions + }), app; // Check for an actual class, otherwise just use whatever was returned @@ -41,7 +45,10 @@ function getAppByName(name) { app = AppClass; } - return app; + return { + app: app, + proxy: appProxy + }; } // The loader is responsible for loading apps @@ -63,7 +70,9 @@ loader = { }); }) .then(function (appPerms) { - var app = getAppByName(name, appPerms); + var appInfo = getAppByName(name, appPerms), + app = appInfo.app, + appProxy = appInfo.proxy; // Check for an install() method on the app. if (!_.isFunction(app.install)) { @@ -84,7 +93,9 @@ loader = { var perms = new AppPermissions(getAppAbsolutePath(name)); return perms.read().then(function (appPerms) { - var app = getAppByName(name, appPerms); + var appInfo = getAppByName(name, appPerms), + app = appInfo.app, + appProxy = appInfo.proxy; // Check for an activate() method on the app. if (!_.isFunction(app.activate)) { diff --git a/core/server/apps/proxy.js b/core/server/apps/proxy.js index fdf6e2e48b..97b1424750 100644 --- a/core/server/apps/proxy.js +++ b/core/server/apps/proxy.js @@ -3,22 +3,81 @@ var _ = require('lodash'), helpers = require('../helpers'), filters = require('../filters'); -var proxy = { +var generateProxyFunctions = function (name, permissions) { + var getPermission = function (perm) { + return permissions[perm]; + }, + getPermissionToMethod = function (perm, method) { + var perms = getPermission(perm); - filters: { - register: filters.registerFilter.bind(filters), - deregister: filters.deregisterFilter.bind(filters) - }, - helpers: { - register: helpers.registerThemeHelper.bind(helpers), - registerAsync: helpers.registerAsyncThemeHelper.bind(helpers) - }, - api: { - posts: _.pick(api.posts, 'browse', 'read'), - tags: _.pick(api.tags, 'browse'), - notifications: _.pick(api.notifications, 'add'), - settings: _.pick(api.settings, 'read') - } + if (!perms) { + return false; + } + + return _.find(perms, function (name) { + return name === method; + }); + }, + runIfPermissionToMethod = function (perm, method, wrappedFunc, context, args) { + var permValue = getPermissionToMethod(perm, method); + + if (!permValue) { + throw new Error('The App "' + name + '" attempted to perform an action or access a resource (' + perm + '.' + method + ') without permission.'); + } + + return wrappedFunc.apply(context, args); + }, + checkRegisterPermissions = function (perm, registerMethod) { + return _.wrap(registerMethod, function (origRegister, name) { + return runIfPermissionToMethod(perm, name, origRegister, this, _.toArray(arguments).slice(1)); + }); + }, + passThruAppContextToApi = function (perm, apiMethods) { + var appContext = { + app: name + }; + + return _.reduce(apiMethods, function (memo, apiMethod, methodName) { + memo[methodName] = function () { + return apiMethod.apply(_.clone(appContext), _.toArray(arguments)); + }; + + return memo; + }, {}); + }, + proxy; + + proxy = { + filters: { + register: checkRegisterPermissions('filters', filters.registerFilter.bind(filters)), + deregister: checkRegisterPermissions('filters', filters.deregisterFilter.bind(filters)) + }, + helpers: { + register: checkRegisterPermissions('helpers', helpers.registerThemeHelper.bind(helpers)), + registerAsync: checkRegisterPermissions('helpers', helpers.registerAsyncThemeHelper.bind(helpers)) + }, + api: { + posts: passThruAppContextToApi('posts', _.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy')), + tags: passThruAppContextToApi('tags', _.pick(api.tags, 'browse')), + notifications: passThruAppContextToApi('notifications', _.pick(api.notifications, 'browse', 'add', 'destroy')), + settings: passThruAppContextToApi('settings', _.pick(api.settings, 'browse', 'read', 'edit')) + } + }; + + return proxy; }; -module.exports = proxy; +function AppProxy(options) { + + if (!options.name) { + throw new Error('Must provide an app name for api context'); + } + + if (!options.permissions) { + throw new Error('Must provide app permissions'); + } + + _.extend(this, generateProxyFunctions(options.name, options.permissions)); +} + +module.exports = AppProxy; diff --git a/core/test/unit/apps_spec.js b/core/test/unit/apps_spec.js index defd2d3510..722699a4c0 100644 --- a/core/test/unit/apps_spec.js +++ b/core/test/unit/apps_spec.js @@ -8,12 +8,13 @@ var fs = require('fs'), when = require('when'), helpers = require('../../server/helpers'), filters = require('../../server/filters'), + api = require('../../server/api'), // Stuff we are testing - appProxy = require('../../server/apps/proxy'), - AppSandbox = require('../../server/apps/sandbox'), + AppProxy = require('../../server/apps/proxy'), + AppSandbox = require('../../server/apps/sandbox'), AppDependencies = require('../../server/apps/dependencies'), - AppPermissions = require('../../server/apps/permissions'); + AppPermissions = require('../../server/apps/permissions'); describe('Apps', function () { @@ -56,7 +57,34 @@ describe('Apps', function () { }); describe('Proxy', function () { + it('requires a name to be passed', function () { + function makeWithoutName() { + return new AppProxy({}); + } + + makeWithoutName.should.throw('Must provide an app name for api context'); + }); + + it('requires permissions to be passed', function () { + function makeWithoutPerms() { + return new AppProxy({ + name: 'NoPerms' + }); + } + + makeWithoutPerms.should.throw('Must provide app permissions'); + }); + it('creates a ghost proxy', function () { + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + should.exist(appProxy.filters); should.exist(appProxy.filters.register); should.exist(appProxy.filters.deregister); @@ -68,20 +96,173 @@ describe('Apps', function () { should.exist(appProxy.api); should.exist(appProxy.api.posts); - should.not.exist(appProxy.api.posts.edit); - should.not.exist(appProxy.api.posts.add); - should.not.exist(appProxy.api.posts.destroy); + should.exist(appProxy.api.posts.browse); + should.exist(appProxy.api.posts.read); + should.exist(appProxy.api.posts.edit); + should.exist(appProxy.api.posts.add); + should.exist(appProxy.api.posts.destroy); should.not.exist(appProxy.api.users); should.exist(appProxy.api.tags); + should.exist(appProxy.api.tags.browse); should.exist(appProxy.api.notifications); - should.not.exist(appProxy.api.notifications.destroy); + should.exist(appProxy.api.notifications.browse); + should.exist(appProxy.api.notifications.add); + should.exist(appProxy.api.notifications.destroy); should.exist(appProxy.api.settings); - should.not.exist(appProxy.api.settings.browse); - should.not.exist(appProxy.api.settings.add); + should.exist(appProxy.api.settings.browse); + should.exist(appProxy.api.settings.read); + should.exist(appProxy.api.settings.edit); + }); + + it('allows filter registration with permission', function (done) { + var registerSpy = sandbox.spy(filters, 'registerFilter'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['testFilter'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + var fakePosts = [{ id: 0 }, { id: 1 }]; + + var filterStub = sandbox.spy(function (val) { + return val; + }); + + appProxy.filters.register('testFilter', 5, filterStub); + + registerSpy.called.should.equal(true); + + filterStub.called.should.equal(false); + + filters.doFilter('testFilter', fakePosts) + .then(function () { + filterStub.called.should.equal(true); + appProxy.filters.deregister('testFilter', 5, filterStub); + done(); + }) + .otherwise(done); + }); + + it('does not allow filter registration without permission', function () { + var registerSpy = sandbox.spy(filters, 'registerFilter'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + var filterStub = sandbox.stub().returns('test result'); + + function registerFilterWithoutPermission() { + appProxy.filters.register('superSecretFilter', 5, filterStub); + } + + registerFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or access a resource (filters.superSecretFilter) without permission.'); + + registerSpy.called.should.equal(false); + }); + + it('allows filter deregistration with permission', function (done) { + var registerSpy = sandbox.spy(filters, 'deregisterFilter'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostsRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + var fakePosts = [{ id: 0 }, { id: 1 }]; + + var filterStub = sandbox.stub().returns(fakePosts); + + appProxy.filters.deregister('prePostsRender', 5, filterStub); + + registerSpy.called.should.equal(true); + + filterStub.called.should.equal(false); + + filters.doFilter('prePostsRender', fakePosts) + .then(function () { + filterStub.called.should.equal(false); + done(); + }) + .otherwise(done); + }); + + it('does not allow filter deregistration without permission', function () { + var registerSpy = sandbox.spy(filters, 'deregisterFilter'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + var filterStub = sandbox.stub().returns('test result'); + + function deregisterFilterWithoutPermission() { + appProxy.filters.deregister('superSecretFilter', 5, filterStub); + } + + deregisterFilterWithoutPermission.should.throw('The App "TestApp" attempted to perform an action or access a resource (filters.superSecretFilter) without permission.'); + + registerSpy.called.should.equal(false); + }); + + it('allows helper registration with permission', function () { + var registerSpy = sandbox.spy(helpers, 'registerThemeHelper'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + appProxy.helpers.register('myTestHelper', sandbox.stub().returns('test result')); + + registerSpy.called.should.equal(true); + }); + + it('does not allow helper registration without permission', function () { + var registerSpy = sandbox.spy(helpers, 'registerThemeHelper'); + + var appProxy = new AppProxy({ + name: 'TestApp', + permissions: { + filters: ['prePostRender'], + helpers: ['myTestHelper'], + posts: ['browse', 'read', 'edit', 'add', 'delete'] + } + }); + + function registerWithoutPermissions() { + appProxy.helpers.register('otherHelper', sandbox.stub().returns('test result')); + } + + registerWithoutPermissions.should.throw('The App "TestApp" attempted to perform an action or access a resource (helpers.otherHelper) without permission.'); + + registerSpy.called.should.equal(false); }); }); @@ -90,6 +271,10 @@ describe('Apps', function () { var appBox = new AppSandbox(), appPath = path.resolve(__dirname, '..', 'utils', 'fixtures', 'app', 'good.js'), GoodApp, + appProxy = new AppProxy({ + name: 'TestApp', + permissions: {} + }), app; GoodApp = appBox.loadApp(appPath); @@ -122,6 +307,10 @@ describe('Apps', function () { var appBox = new AppSandbox(), badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badinstall.js'), BadApp, + appProxy = new AppProxy({ + name: 'TestApp', + permissions: {} + }), app, installApp = function () { app.install(appProxy);