Merge pull request #2631 from jgable/appProxyContext

AppProxy with permissions checks and app context
This commit is contained in:
Hannah Wolfe 2014-04-28 12:17:29 +01:00
commit b82ebac44c
3 changed files with 289 additions and 30 deletions

View File

@ -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)) {

View File

@ -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;

View File

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