mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Merge pull request #2631 from jgable/appProxyContext
AppProxy with permissions checks and app context
This commit is contained in:
commit
b82ebac44c
3 changed files with 289 additions and 30 deletions
|
@ -2,7 +2,7 @@
|
||||||
var path = require('path'),
|
var path = require('path'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
when = require('when'),
|
when = require('when'),
|
||||||
appProxy = require('./proxy'),
|
AppProxy = require('./proxy'),
|
||||||
config = require('../config'),
|
config = require('../config'),
|
||||||
AppSandbox = require('./sandbox'),
|
AppSandbox = require('./sandbox'),
|
||||||
AppDependencies = require('./dependencies'),
|
AppDependencies = require('./dependencies'),
|
||||||
|
@ -29,9 +29,13 @@ function loadApp(appPath) {
|
||||||
return sandbox.loadApp(appPath);
|
return sandbox.loadApp(appPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppByName(name) {
|
function getAppByName(name, permissions) {
|
||||||
// Grab the app class to instantiate
|
// Grab the app class to instantiate
|
||||||
var AppClass = loadApp(getAppRelativePath(name)),
|
var AppClass = loadApp(getAppRelativePath(name)),
|
||||||
|
appProxy = new AppProxy({
|
||||||
|
name: name,
|
||||||
|
permissions: permissions
|
||||||
|
}),
|
||||||
app;
|
app;
|
||||||
|
|
||||||
// Check for an actual class, otherwise just use whatever was returned
|
// Check for an actual class, otherwise just use whatever was returned
|
||||||
|
@ -41,7 +45,10 @@ function getAppByName(name) {
|
||||||
app = AppClass;
|
app = AppClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return {
|
||||||
|
app: app,
|
||||||
|
proxy: appProxy
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The loader is responsible for loading apps
|
// The loader is responsible for loading apps
|
||||||
|
@ -63,7 +70,9 @@ loader = {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function (appPerms) {
|
.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.
|
// Check for an install() method on the app.
|
||||||
if (!_.isFunction(app.install)) {
|
if (!_.isFunction(app.install)) {
|
||||||
|
@ -84,7 +93,9 @@ loader = {
|
||||||
var perms = new AppPermissions(getAppAbsolutePath(name));
|
var perms = new AppPermissions(getAppAbsolutePath(name));
|
||||||
|
|
||||||
return perms.read().then(function (appPerms) {
|
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.
|
// Check for an activate() method on the app.
|
||||||
if (!_.isFunction(app.activate)) {
|
if (!_.isFunction(app.activate)) {
|
||||||
|
|
|
@ -3,22 +3,81 @@ var _ = require('lodash'),
|
||||||
helpers = require('../helpers'),
|
helpers = require('../helpers'),
|
||||||
filters = require('../filters');
|
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: {
|
if (!perms) {
|
||||||
register: filters.registerFilter.bind(filters),
|
return false;
|
||||||
deregister: filters.deregisterFilter.bind(filters)
|
}
|
||||||
},
|
|
||||||
helpers: {
|
return _.find(perms, function (name) {
|
||||||
register: helpers.registerThemeHelper.bind(helpers),
|
return name === method;
|
||||||
registerAsync: helpers.registerAsyncThemeHelper.bind(helpers)
|
});
|
||||||
},
|
},
|
||||||
api: {
|
runIfPermissionToMethod = function (perm, method, wrappedFunc, context, args) {
|
||||||
posts: _.pick(api.posts, 'browse', 'read'),
|
var permValue = getPermissionToMethod(perm, method);
|
||||||
tags: _.pick(api.tags, 'browse'),
|
|
||||||
notifications: _.pick(api.notifications, 'add'),
|
if (!permValue) {
|
||||||
settings: _.pick(api.settings, 'read')
|
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'),
|
when = require('when'),
|
||||||
helpers = require('../../server/helpers'),
|
helpers = require('../../server/helpers'),
|
||||||
filters = require('../../server/filters'),
|
filters = require('../../server/filters'),
|
||||||
|
api = require('../../server/api'),
|
||||||
|
|
||||||
// Stuff we are testing
|
// Stuff we are testing
|
||||||
appProxy = require('../../server/apps/proxy'),
|
AppProxy = require('../../server/apps/proxy'),
|
||||||
AppSandbox = require('../../server/apps/sandbox'),
|
AppSandbox = require('../../server/apps/sandbox'),
|
||||||
AppDependencies = require('../../server/apps/dependencies'),
|
AppDependencies = require('../../server/apps/dependencies'),
|
||||||
AppPermissions = require('../../server/apps/permissions');
|
AppPermissions = require('../../server/apps/permissions');
|
||||||
|
|
||||||
describe('Apps', function () {
|
describe('Apps', function () {
|
||||||
|
|
||||||
|
@ -56,7 +57,34 @@ describe('Apps', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Proxy', 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 () {
|
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);
|
||||||
should.exist(appProxy.filters.register);
|
should.exist(appProxy.filters.register);
|
||||||
should.exist(appProxy.filters.deregister);
|
should.exist(appProxy.filters.deregister);
|
||||||
|
@ -68,20 +96,173 @@ describe('Apps', function () {
|
||||||
should.exist(appProxy.api);
|
should.exist(appProxy.api);
|
||||||
|
|
||||||
should.exist(appProxy.api.posts);
|
should.exist(appProxy.api.posts);
|
||||||
should.not.exist(appProxy.api.posts.edit);
|
should.exist(appProxy.api.posts.browse);
|
||||||
should.not.exist(appProxy.api.posts.add);
|
should.exist(appProxy.api.posts.read);
|
||||||
should.not.exist(appProxy.api.posts.destroy);
|
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.not.exist(appProxy.api.users);
|
||||||
|
|
||||||
should.exist(appProxy.api.tags);
|
should.exist(appProxy.api.tags);
|
||||||
|
should.exist(appProxy.api.tags.browse);
|
||||||
|
|
||||||
should.exist(appProxy.api.notifications);
|
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.exist(appProxy.api.settings);
|
||||||
should.not.exist(appProxy.api.settings.browse);
|
should.exist(appProxy.api.settings.browse);
|
||||||
should.not.exist(appProxy.api.settings.add);
|
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(),
|
var appBox = new AppSandbox(),
|
||||||
appPath = path.resolve(__dirname, '..', 'utils', 'fixtures', 'app', 'good.js'),
|
appPath = path.resolve(__dirname, '..', 'utils', 'fixtures', 'app', 'good.js'),
|
||||||
GoodApp,
|
GoodApp,
|
||||||
|
appProxy = new AppProxy({
|
||||||
|
name: 'TestApp',
|
||||||
|
permissions: {}
|
||||||
|
}),
|
||||||
app;
|
app;
|
||||||
|
|
||||||
GoodApp = appBox.loadApp(appPath);
|
GoodApp = appBox.loadApp(appPath);
|
||||||
|
@ -122,6 +307,10 @@ describe('Apps', function () {
|
||||||
var appBox = new AppSandbox(),
|
var appBox = new AppSandbox(),
|
||||||
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badinstall.js'),
|
badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badinstall.js'),
|
||||||
BadApp,
|
BadApp,
|
||||||
|
appProxy = new AppProxy({
|
||||||
|
name: 'TestApp',
|
||||||
|
permissions: {}
|
||||||
|
}),
|
||||||
app,
|
app,
|
||||||
installApp = function () {
|
installApp = function () {
|
||||||
app.install(appProxy);
|
app.install(appProxy);
|
||||||
|
|
Loading…
Add table
Reference in a new issue