mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-05 18:34:39 +03:00
Merge pull request #2631 from jgable/appProxyContext
AppProxy with permissions checks and app context
This commit is contained in:
commit
b82ebac44c
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user