Revert "Remove Apps"

This reverts commit cbb59a57db.
This commit is contained in:
Hannah Wolfe 2020-03-20 08:58:26 +00:00
parent cbb59a57db
commit bc7906a7b2
46 changed files with 798 additions and 58 deletions

View File

@ -1,6 +1,6 @@
// This file defines everything that helpers "require" // This file defines everything that helpers "require"
// With the exception of modules like lodash, Bluebird // With the exception of modules like lodash, Bluebird
// We can later refactor to enforce this something like we did in apps // We can later refactor to enforce this something like we do in apps
var hbs = require('../services/themes/engine'), var hbs = require('../services/themes/engine'),
settingsCache = require('../../server/services/settings/cache'), settingsCache = require('../../server/services/settings/cache'),
config = require('../../server/config'); config = require('../../server/config');

View File

@ -58,7 +58,7 @@ module.exports.init = (options = {start: false}) => {
* 3. Taxonomies: Stronger than collections, because it's an inbuilt feature. * 3. Taxonomies: Stronger than collections, because it's an inbuilt feature.
* 4. Collections * 4. Collections
* 5. Static Pages: Weaker than collections, because we first try to find a post slug and fallback to lookup a static page. * 5. Static Pages: Weaker than collections, because we first try to find a post slug and fallback to lookup a static page.
* 6. Internal Apps: Weakest * 6. Apps: Weakest
*/ */
module.exports.start = (apiVersion) => { module.exports.start = (apiVersion) => {
const RESOURCE_CONFIG = require(`./config/${apiVersion}`); const RESOURCE_CONFIG = require(`./config/${apiVersion}`);

View File

@ -4,7 +4,8 @@ const common = require('../../lib/common');
const allowedTypes = { const allowedTypes = {
post: models.Post, post: models.Post,
tag: models.Tag, tag: models.Tag,
user: models.User user: models.User,
app: models.App
}; };
module.exports = { module.exports = {

View File

@ -4,7 +4,8 @@ const common = require('../../lib/common');
const allowedTypes = { const allowedTypes = {
post: models.Post, post: models.Post,
tag: models.Tag, tag: models.Tag,
user: models.User user: models.User,
app: models.App
}; };
module.exports = { module.exports = {

View File

@ -1,11 +1,10 @@
const debug = require('ghost-ignition').debug('importer:settings'); const debug = require('ghost-ignition').debug('importer:settings'),
const Promise = require('bluebird'); Promise = require('bluebird'),
const _ = require('lodash'); _ = require('lodash'),
const BaseImporter = require('./base'); BaseImporter = require('./base'),
const models = require('../../../../models'); models = require('../../../../models'),
const defaultSettings = require('../../../schema').defaultSettings; defaultSettings = require('../../../schema').defaultSettings,
const labsDefaults = JSON.parse(defaultSettings.blog.labs.defaultValue); labsDefaults = JSON.parse(defaultSettings.blog.labs.defaultValue);
const deprecatedSettings = ['active_apps', 'installed_apps'];
const isFalse = (value) => { const isFalse = (value) => {
// Catches false, null, undefined, empty string // Catches false, null, undefined, empty string
@ -66,9 +65,27 @@ class SettingsImporter extends BaseImporter {
}); });
} }
// Don't import any old, deprecated settings const activeApps = _.find(this.dataToImport, {key: 'active_apps'});
const installedApps = _.find(this.dataToImport, {key: 'installed_apps'});
const hasValueEntries = (setting = {}) => {
try {
return JSON.parse(setting.value || '[]').length !== 0;
} catch (e) {
return false;
}
};
if (hasValueEntries(activeApps) || hasValueEntries(installedApps)) {
this.problems.push({
message: 'Old settings for apps were not imported',
help: this.modelName,
context: JSON.stringify({activeApps, installedApps})
});
}
this.dataToImport = _.filter(this.dataToImport, (data) => { this.dataToImport = _.filter(this.dataToImport, (data) => {
return !_.includes(deprecatedSettings, data.key); return data.key !== 'active_apps' && data.key !== 'installed_apps';
}); });
const permalinks = _.find(this.dataToImport, {key: 'permalinks'}); const permalinks = _.find(this.dataToImport, {key: 'permalinks'});

View File

@ -152,7 +152,7 @@ module.exports = {
maxlength: 50, maxlength: 50,
nullable: false, nullable: false,
defaultTo: 'core', defaultTo: 'core',
validations: {isIn: [['core', 'blog', 'theme', 'private', 'members', 'bulk_email']]} validations: {isIn: [['core', 'blog', 'theme', 'app', 'plugin', 'private', 'members', 'bulk_email']]}
}, },
created_at: {type: 'dateTime', nullable: false}, created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'string', maxlength: 24, nullable: false}, created_by: {type: 'string', maxlength: 24, nullable: false},

View File

@ -30,6 +30,7 @@ function initialiseServices() {
routing.bootstrap.start(themeService.getApiVersion()); routing.bootstrap.start(themeService.getApiVersion());
const permissions = require('./services/permissions'), const permissions = require('./services/permissions'),
apps = require('./services/apps'),
xmlrpc = require('./services/xmlrpc'), xmlrpc = require('./services/xmlrpc'),
slack = require('./services/slack'), slack = require('./services/slack'),
{mega} = require('./services/mega'), {mega} = require('./services/mega'),
@ -45,6 +46,7 @@ function initialiseServices() {
slack.listen(), slack.listen(),
mega.listen(), mega.listen(),
webhooks.listen(), webhooks.listen(),
apps.init(),
scheduling.init({ scheduling.init({
schedulerUrl: config.get('scheduling').schedulerUrl, schedulerUrl: config.get('scheduling').schedulerUrl,
active: config.get('scheduling').active, active: config.get('scheduling').active,
@ -55,7 +57,7 @@ function initialiseServices() {
contentPath: config.getContentPath('scheduling') contentPath: config.getContentPath('scheduling')
}) })
).then(function () { ).then(function () {
debug('XMLRPC, Slack, MEGA, Webhooks, Scheduling, Permissions done'); debug('XMLRPC, Slack, MEGA, Webhooks, Apps, Scheduling, Permissions done');
// Initialise analytics events // Initialise analytics events
if (config.get('segment:key')) { if (config.get('segment:key')) {

View File

@ -4,7 +4,7 @@ var _ = require('lodash'),
/** /**
* ### Filter Packages * ### Filter Packages
* Normalizes packages read by read-packages so that the themes module can use them. * Normalizes packages read by read-packages so that the apps and themes modules can use them.
* Iterates over each package and return an array of objects which are simplified representations of the package * Iterates over each package and return an array of objects which are simplified representations of the package
* with 3 properties: * with 3 properties:
* - `name` - the package name * - `name` - the package name
@ -17,10 +17,10 @@ var _ = require('lodash'),
* *
* @param {object} packages as returned by read-packages * @param {object} packages as returned by read-packages
* @param {array/string} active as read from the settings object * @param {array/string} active as read from the settings object
* @returns {Array} of objects with useful info about themes * @returns {Array} of objects with useful info about apps / themes
*/ */
filterPackages = function filterPackages(packages, active) { filterPackages = function filterPackages(packages, active) {
// turn active into an array if it isn't one, so this function can deal with lists and one-offs // turn active into an array (so themes and apps can be checked the same)
if (!Array.isArray(active)) { if (!Array.isArray(active)) {
active = [active]; active = [active];
} }

View File

@ -3,7 +3,9 @@
* *
* Ghost has / is in the process of gaining support for several different types of sub-packages: * Ghost has / is in the process of gaining support for several different types of sub-packages:
* - Themes: have always been packages, but we're going to lean more heavily on npm & package.json in future * - Themes: have always been packages, but we're going to lean more heavily on npm & package.json in future
* - Adapters: replace fundamental pieces like storage, will become npm modules * - Adapters: an early version of apps, replace fundamental pieces like storage, will become npm modules
* - Apps: plugins that can be installed whilst Ghost is running & modify behaviour
* - More?
* *
* These utils facilitate loading, reading, managing etc, packages from the file system. * These utils facilitate loading, reading, managing etc, packages from the file system.
*/ */

View File

@ -0,0 +1,20 @@
var ghostBookshelf = require('./base'),
AppField,
AppFields;
AppField = ghostBookshelf.Model.extend({
tableName: 'app_fields',
post: function post() {
return this.morphOne('Post', 'relatable');
}
});
AppFields = ghostBookshelf.Collection.extend({
model: AppField
});
module.exports = {
AppField: ghostBookshelf.model('AppField', AppField),
AppFields: ghostBookshelf.collection('AppFields', AppFields)
};

View File

@ -0,0 +1,20 @@
var ghostBookshelf = require('./base'),
AppSetting,
AppSettings;
AppSetting = ghostBookshelf.Model.extend({
tableName: 'app_settings',
app: function app() {
return this.belongsTo('App');
}
});
AppSettings = ghostBookshelf.Collection.extend({
model: AppSetting
});
module.exports = {
AppSetting: ghostBookshelf.model('AppSetting', AppSetting),
AppSettings: ghostBookshelf.collection('AppSettings', AppSettings)
};

60
core/server/models/app.js Normal file
View File

@ -0,0 +1,60 @@
var ghostBookshelf = require('./base'),
App,
Apps;
App = ghostBookshelf.Model.extend({
tableName: 'apps',
onSaving: function onSaving(newPage, attr, options) {
var self = this;
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
if (this.hasChanged('slug') || !this.get('slug')) {
// Pass the new slug through the generator to strip illegal characters, detect duplicates
return ghostBookshelf.Model.generateSlug(App, this.get('slug') || this.get('name'),
{transacting: options.transacting})
.then(function then(slug) {
self.set({slug: slug});
});
}
},
permissions: function permissions() {
return this.belongsToMany('Permission', 'permissions_apps');
},
settings: function settings() {
return this.belongsToMany('AppSetting', 'app_settings');
}
}, {
/**
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
* @param {String} methodName The name of the method to check valid options for.
* @return {Array} Keys allowed in the `options` hash of the model's method.
*/
permittedOptions: function permittedOptions(methodName) {
var options = ghostBookshelf.Model.permittedOptions.call(this, methodName),
// whitelists for the `options` hash argument on methods, by method name.
// these are the only options that can be passed to Bookshelf / Knex.
validOptions = {
findOne: ['withRelated']
};
if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
return options;
}
});
Apps = ghostBookshelf.Collection.extend({
model: App
});
module.exports = {
App: ghostBookshelf.model('App', App),
Apps: ghostBookshelf.collection('Apps', Apps)
};

View File

@ -3,9 +3,8 @@
// several basic behaviours such as UUIDs, as well as a set of Data methods for accessing information from the database. // several basic behaviours such as UUIDs, as well as a set of Data methods for accessing information from the database.
// //
// The models are internal to Ghost, only the API and some internal functions such as migration and import/export // The models are internal to Ghost, only the API and some internal functions such as migration and import/export
// accesses the models directly. // accesses the models directly. All other parts of Ghost, including the blog frontend, admin UI, and apps are only
// allowed to access data via the API.
// All other parts of Ghost, including the frontend & admin UI are only allowed to access data via the API.
const _ = require('lodash'), const _ = require('lodash'),
bookshelf = require('bookshelf'), bookshelf = require('bookshelf'),
moment = require('moment'), moment = require('moment'),

View File

@ -15,6 +15,9 @@ require('./base/listeners');
exports = module.exports; exports = module.exports;
models = [ models = [
'app-field',
'app-setting',
'app',
'permission', 'permission',
'post', 'post',
'role', 'role',

View File

@ -42,11 +42,11 @@ Invite = ghostBookshelf.Model.extend({
return ghostBookshelf.Model.add.call(this, data, options); return ghostBookshelf.Model.add.call(this, data, options);
}, },
permissible(inviteModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible(inviteModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
const isAdd = (action === 'add'); const isAdd = (action === 'add');
if (!isAdd) { if (!isAdd) {
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
return Promise.resolve(); return Promise.resolve();
} }
@ -86,7 +86,7 @@ Invite = ghostBookshelf.Model.extend({
}); });
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -33,6 +33,10 @@ Permission = ghostBookshelf.Model.extend({
users: function users() { users: function users() {
return this.belongsToMany('User'); return this.belongsToMany('User');
},
apps: function apps() {
return this.belongsToMany('App');
} }
}); });

View File

@ -934,7 +934,7 @@ Post = ghostBookshelf.Model.extend({
}, },
// NOTE: the `authors` extension is the parent of the post model. It also has a permissible function. // NOTE: the `authors` extension is the parent of the post model. It also has a permissible function.
permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
let isContributor; let isContributor;
let isOwner; let isOwner;
let isAdmin; let isAdmin;
@ -989,7 +989,7 @@ Post = ghostBookshelf.Model.extend({
excludedAttrs.push('tags'); excludedAttrs.push('tags');
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve({excludedAttrs}); return Promise.resolve({excludedAttrs});
} }

View File

@ -331,7 +331,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
return destroyPost(); return destroyPost();
}, },
permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
var self = this, var self = this,
postModel = postModelOrId, postModel = postModelOrId,
origArgs, isContributor, isAuthor, isEdit, isAdd, isDestroy; origArgs, isContributor, isAuthor, isEdit, isAdd, isDestroy;
@ -420,7 +420,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
hasUserPermission = hasUserPermission || isPrimaryAuthor(); hasUserPermission = hasUserPermission || isPrimaryAuthor();
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Post.permissible.call( return Post.permissible.call(
this, this,
postModelOrId, postModelOrId,
@ -428,6 +428,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
unsafeAttrs, unsafeAttrs,
loadedPermissions, loadedPermissions,
hasUserPermission, hasUserPermission,
hasAppPermission,
hasApiKeyPermission hasApiKeyPermission
).then(({excludedAttrs}) => { ).then(({excludedAttrs}) => {
// @TODO: we need a concept for making a diff between incoming authors and existing authors // @TODO: we need a concept for making a diff between incoming authors and existing authors

View File

@ -50,7 +50,7 @@ Role = ghostBookshelf.Model.extend({
return options; return options;
}, },
permissible: function permissible(roleModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible: function permissible(roleModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
// If we passed in an id instead of a model, get the model // If we passed in an id instead of a model, get the model
// then check the permissions // then check the permissions
if (_.isNumber(roleModelOrId) || _.isString(roleModelOrId)) { if (_.isNumber(roleModelOrId) || _.isString(roleModelOrId)) {
@ -95,7 +95,7 @@ Role = ghostBookshelf.Model.extend({
} }
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -251,7 +251,7 @@ Settings = ghostBookshelf.Model.extend({
}); });
}, },
permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
let isEdit = (action === 'edit'); let isEdit = (action === 'edit');
let isOwner; let isOwner;
@ -271,7 +271,7 @@ Settings = ghostBookshelf.Model.extend({
hasUserPermission = isOwner; hasUserPermission = isOwner;
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -648,7 +648,7 @@ User = ghostBookshelf.Model.extend({
}); });
}, },
permissible: function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) { permissible: function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
var self = this, var self = this,
userModel = userModelOrId, userModel = userModelOrId,
origArgs; origArgs;
@ -738,7 +738,7 @@ User = ghostBookshelf.Model.extend({
.then((owner) => { .then((owner) => {
// CASE: owner can assign role to any user // CASE: owner can assign role to any user
if (context.user === owner.id) { if (context.user === owner.id) {
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve(); return Promise.resolve();
} }
@ -760,7 +760,7 @@ User = ghostBookshelf.Model.extend({
// e.g. admin can assign admin role to a user, but not owner // e.g. admin can assign admin role to a user, but not owner
return permissions.canThis(context).assign.role(role) return permissions.canThis(context).assign.role(role)
.then(() => { .then(() => {
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve(); return Promise.resolve();
} }
@ -770,7 +770,7 @@ User = ghostBookshelf.Model.extend({
}); });
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve(); return Promise.resolve();
} }
@ -780,7 +780,7 @@ User = ghostBookshelf.Model.extend({
}); });
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -0,0 +1,21 @@
const debug = require('ghost-ignition').debug('services:apps');
const Promise = require('bluebird');
const common = require('../../lib/common');
const config = require('../../config');
const loader = require('./loader');
module.exports = {
init: function () {
debug('init begin');
const appsToLoad = config.get('apps:internal');
return Promise.map(appsToLoad, appName => loader.activateAppByName(appName))
.catch(function (err) {
common.logging.error(new common.errors.GhostError({
err: err,
context: common.i18n.t('errors.apps.appWillNotBeLoaded.error'),
help: common.i18n.t('errors.apps.appWillNotBeLoaded.help')
}));
});
}
};

View File

@ -0,0 +1,45 @@
const path = require('path');
const _ = require('lodash');
const Promise = require('bluebird');
const common = require('../../lib/common');
const config = require('../../config');
const Proxy = require('./proxy');
// Get the full path to an app by name
function getAppAbsolutePath(name) {
return path.join(config.get('paths').internalAppPath, name);
}
function loadApp(name) {
return require(getAppAbsolutePath(name));
}
function getAppByName(name) {
// Grab the app class to instantiate
const AppClass = loadApp(name);
const proxy = Proxy.getInstance();
// Check for an actual class, otherwise just use whatever was returned
const app = _.isFunction(AppClass) ? new AppClass(proxy) : AppClass;
return {
app,
proxy
};
}
module.exports = {
// Activate a app and return it
activateAppByName: function (name) {
const {app, proxy} = getAppByName(name);
// Check for an activate() method on the app.
if (!_.isFunction(app.activate)) {
return Promise.reject(new Error(common.i18n.t('errors.apps.noActivateMethodLoadingApp.error', {name: name})));
}
// Wrapping the activate() with a when because it's possible
// to not return a promise from it.
return Promise.resolve(app.activate(proxy)).return(app);
}
};

View File

@ -0,0 +1,18 @@
const helpers = require('../../../frontend/helpers/register');
const routingService = require('../../../frontend/services/routing');
module.exports.getInstance = function getInstance() {
const appRouter = routingService.registry.getRouter('appRouter');
return {
helpers: {
register: helpers.registerThemeHelper.bind(helpers),
registerAsync: helpers.registerAsyncThemeHelper.bind(helpers)
},
// Expose the route service...
routeService: {
// This allows for mounting an entirely new Router at a path...
registerRouter: appRouter.mountRouter.bind(appRouter)
}
};
};

View File

@ -50,8 +50,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
// Iterate through the user permissions looking for an affirmation // Iterate through the user permissions looking for an affirmation
var userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null, var userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null,
apiKeyPermissions = loadedPermissions.apiKey ? loadedPermissions.apiKey.permissions : null, apiKeyPermissions = loadedPermissions.apiKey ? loadedPermissions.apiKey.permissions : null,
appPermissions = loadedPermissions.app ? loadedPermissions.app.permissions : null,
hasUserPermission, hasUserPermission,
hasApiKeyPermission, hasApiKeyPermission,
hasAppPermission,
checkPermission = function (perm) { checkPermission = function (perm) {
var permObjId; var permObjId;
@ -89,14 +91,20 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
hasApiKeyPermission = _.some(apiKeyPermissions, checkPermission); hasApiKeyPermission = _.some(apiKeyPermissions, checkPermission);
} }
// Check app permissions if they were passed
hasAppPermission = true;
if (!_.isNull(appPermissions)) {
hasAppPermission = _.some(appPermissions, checkPermission);
}
// Offer a chance for the TargetModel to override the results // Offer a chance for the TargetModel to override the results
if (TargetModel && _.isFunction(TargetModel.permissible)) { if (TargetModel && _.isFunction(TargetModel.permissible)) {
return TargetModel.permissible( return TargetModel.permissible(
modelId, actType, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission modelId, actType, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission
); );
} }
if (hasUserPermission && hasApiKeyPermission) { if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return; return;
} }
@ -112,6 +120,7 @@ CanThisResult.prototype.beginCheck = function (context) {
var self = this, var self = this,
userPermissionLoad, userPermissionLoad,
apiKeyPermissionLoad, apiKeyPermissionLoad,
appPermissionLoad,
permissionsLoad; permissionsLoad;
// Get context.user, context.api_key and context.app // Get context.user, context.api_key and context.app
@ -137,11 +146,20 @@ CanThisResult.prototype.beginCheck = function (context) {
apiKeyPermissionLoad = Promise.resolve(null); apiKeyPermissionLoad = Promise.resolve(null);
} }
// Kick off loading of app permissions if necessary
if (context.app) {
appPermissionLoad = providers.app(context.app);
} else {
// Resolve null if no context.app
appPermissionLoad = Promise.resolve(null);
}
// Wait for both user and app permissions to load // Wait for both user and app permissions to load
permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad]).then(function (result) { permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad, appPermissionLoad]).then(function (result) {
return { return {
user: result[0], user: result[0],
apiKey: result[1] apiKey: result[1],
app: result[2]
}; };
}); });

View File

@ -3,14 +3,16 @@
* *
* Utility function, to expand strings out into objects. * Utility function, to expand strings out into objects.
* @param {Object|String} context * @param {Object|String} context
* @return {{internal: boolean, external: boolean, user: integer|null, public: boolean, api_key: Object|null}} * @return {{internal: boolean, external: boolean, user: integer|null, app: integer|null, public: boolean, api_key: Object|null}}
*/ */
module.exports = function parseContext(context) { module.exports = function parseContext(context) {
// Parse what's passed to canThis.beginCheck for standard user and app scopes
var parsed = { var parsed = {
internal: false, internal: false,
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
integration: null, integration: null,
public: true public: true
}; };
@ -37,5 +39,10 @@ module.exports = function parseContext(context) {
parsed.public = (context.api_key.type === 'content'); parsed.public = (context.api_key.type === 'content');
} }
if (context && context.app) {
parsed.app = context.app;
parsed.public = false;
}
return parsed; return parsed;
}; };

View File

@ -44,6 +44,17 @@ module.exports = {
}); });
}, },
app: function (appName) {
return models.App.findOne({name: appName}, {withRelated: ['permissions']})
.then(function (foundApp) {
if (!foundApp) {
return [];
}
return {permissions: foundApp.related('permissions').models};
});
},
apiKey(id) { apiKey(id) {
return models.ApiKey.findOne({id}, {withRelated: ['role', 'role.permissions']}) return models.ApiKey.findOne({id}, {withRelated: ['role', 'role.permissions']})
.then((foundApiKey) => { .then((foundApiKey) => {

View File

@ -34,6 +34,18 @@
} }
}, },
"errors": { "errors": {
"apps": {
"appWillNotBeLoaded": {
"error": "The app will not be loaded",
"help": "Check with the app creator, or read the app documentation for more details on app requirements"
},
"noActivateMethodLoadingApp": {
"error": "Error loading app named {name}; no activate() method defined."
},
"mustProvideAppName": {
"error": "Must provide an app name for api context"
}
},
"middleware": { "middleware": {
"api": { "api": {
"versionMismatch": "Client request for {clientVersion} does not match server version {serverVersion}." "versionMismatch": "Client request for {clientVersion} does not match server version {serverVersion}."

View File

@ -47,7 +47,7 @@ module.exports = function setupParentApp(options = {}) {
// This sets global res.locals which are needed everywhere // This sets global res.locals which are needed everywhere
parentApp.use(shared.middlewares.ghostLocals); parentApp.use(shared.middlewares.ghostLocals);
// Mount the express apps on the parentApp // Mount the apps on the parentApp
const adminHost = config.get('admin:url') ? (new URL(config.get('admin:url')).hostname) : ''; const adminHost = config.get('admin:url') ? (new URL(config.get('admin:url')).hostname) : '';
const frontendHost = new URL(config.get('url')).hostname; const frontendHost = new URL(config.get('url')).hostname;

View File

@ -7,6 +7,7 @@ const common = require('../../lib/common');
// App requires // App requires
const config = require('../../config'); const config = require('../../config');
const apps = require('../../services/apps');
const constants = require('../../lib/constants'); const constants = require('../../lib/constants');
const storage = require('../../adapters/storage'); const storage = require('../../adapters/storage');
const urlService = require('../../../frontend/services/url'); const urlService = require('../../../frontend/services/url');
@ -155,7 +156,7 @@ module.exports = function setupSiteApp(options = {}) {
siteApp.use(shared.middlewares.servePublicFile('robots.txt', 'text/plain', constants.ONE_HOUR_S)); siteApp.use(shared.middlewares.servePublicFile('robots.txt', 'text/plain', constants.ONE_HOUR_S));
// setup middleware for internal apps // setup middleware for internal apps
// @TODO: refactor this to be a proper app middleware hook for internal apps // @TODO: refactor this to be a proper app middleware hook for internal & external apps
config.get('apps:internal').forEach((appName) => { config.get('apps:internal').forEach((appName) => {
const app = require(path.join(config.get('paths').internalAppPath, appName)); const app = require(path.join(config.get('paths').internalAppPath, appName));
@ -210,6 +211,9 @@ module.exports.reload = () => {
router = siteRoutes({start: themeService.getApiVersion()}); router = siteRoutes({start: themeService.getApiVersion()});
Object.setPrototypeOf(SiteRouter, router); Object.setPrototypeOf(SiteRouter, router);
// re-initialse apps (register app routers, because we have re-initialised the site routers)
apps.init();
// connect routers and resources again // connect routers and resources again
urlService.queue.start({ urlService.queue.start({
event: 'init', event: 'init',

View File

@ -5,6 +5,7 @@ const should = require('should'),
testUtils = require('../../utils'), testUtils = require('../../utils'),
configUtils = require('../../utils/configUtils'), configUtils = require('../../utils/configUtils'),
urlUtils = require('../../utils/urlUtils'), urlUtils = require('../../utils/urlUtils'),
appsService = require('../../../server/services/apps'),
frontendSettingsService = require('../../../frontend/services/settings'), frontendSettingsService = require('../../../frontend/services/settings'),
themeService = require('../../../frontend/services/themes'), themeService = require('../../../frontend/services/themes'),
siteApp = require('../../../server/web/parent-app'); siteApp = require('../../../server/web/parent-app');
@ -22,7 +23,7 @@ describe('Integration - Web - Site', function () {
describe('default routes.yaml', function () { describe('default routes.yaml', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
return testUtils.integrationTesting.initGhost() return testUtils.integrationTesting.initGhost()
@ -32,6 +33,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -1717,7 +1721,7 @@ describe('Integration - Web - Site', function () {
describe('default routes.yaml', function () { describe('default routes.yaml', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
return testUtils.integrationTesting.initGhost() return testUtils.integrationTesting.initGhost()
@ -1727,6 +1731,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -3414,7 +3421,7 @@ describe('Integration - Web - Site', function () {
describe('default routes.yaml', function () { describe('default routes.yaml', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
return testUtils.integrationTesting.initGhost() return testUtils.integrationTesting.initGhost()
@ -3424,6 +3431,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -5110,7 +5120,7 @@ describe('Integration - Web - Site', function () {
describe('no separate admin', function () { describe('no separate admin', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
configUtils.set('url', 'http://example.com'); configUtils.set('url', 'http://example.com');
@ -5123,6 +5133,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -5226,7 +5239,7 @@ describe('Integration - Web - Site', function () {
describe('separate admin host', function () { describe('separate admin host', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
configUtils.set('url', 'http://example.com'); configUtils.set('url', 'http://example.com');
@ -5239,6 +5252,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -5384,7 +5400,7 @@ describe('Integration - Web - Site', function () {
describe('separate admin host w/ admin redirects disabled', function () { describe('separate admin host w/ admin redirects disabled', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
configUtils.set('url', 'http://example.com'); configUtils.set('url', 'http://example.com');
@ -5398,6 +5414,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });
@ -5429,7 +5448,7 @@ describe('Integration - Web - Site', function () {
describe('same host separate protocol', function () { describe('same host separate protocol', function () {
before(function () { before(function () {
testUtils.integrationTesting.urlService.resetGenerators(); testUtils.integrationTesting.urlService.resetGenerators();
testUtils.integrationTesting.defaultMocks(sinon, {amp: true}); testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
testUtils.integrationTesting.overrideGhostConfig(configUtils); testUtils.integrationTesting.overrideGhostConfig(configUtils);
configUtils.set('url', 'http://example.com'); configUtils.set('url', 'http://example.com');
@ -5442,6 +5461,9 @@ describe('Integration - Web - Site', function () {
app = siteApp({start: true}); app = siteApp({start: true});
return testUtils.integrationTesting.urlService.waitTillFinished(); return testUtils.integrationTesting.urlService.waitTillFinished();
})
.then(() => {
return appsService.init();
}); });
}); });

View File

@ -0,0 +1,36 @@
const should = require('should'),
sinon = require('sinon'),
helpers = require('../../../../frontend/helpers/register'),
AppProxy = require('../../../../server/services/apps/proxy'),
routing = require('../../../../frontend/services/routing');
describe('Apps', function () {
beforeEach(function () {
sinon.stub(routing.registry, 'getRouter').withArgs('appRouter').returns({
mountRouter: sinon.stub()
});
});
afterEach(function () {
sinon.restore();
});
describe('Proxy', function () {
it('creates a ghost proxy', function () {
var appProxy = AppProxy.getInstance('TestApp');
should.exist(appProxy.helpers);
should.exist(appProxy.helpers.register);
should.exist(appProxy.helpers.registerAsync);
});
it('allows helper registration', function () {
var registerSpy = sinon.stub(helpers, 'registerThemeHelper'),
appProxy = AppProxy.getInstance('TestApp');
appProxy.helpers.register('myTestHelper', sinon.stub().returns('test result'));
registerSpy.called.should.equal(true);
});
});
});

View File

@ -99,7 +99,7 @@ describe('Permissions', function () {
canThisResult.destroy.user.should.be.a.Function(); canThisResult.destroy.user.should.be.a.Function();
}); });
describe('Non user permissions', function () { describe('Non user/app permissions', function () {
// TODO change to using fake models in tests! // TODO change to using fake models in tests!
// Permissions need to be NOT fundamentally baked into Ghost, but a separate module, at some point // Permissions need to be NOT fundamentally baked into Ghost, but a separate module, at some point
// It can depend on bookshelf, but should NOT use hard coded model knowledge. // It can depend on bookshelf, but should NOT use hard coded model knowledge.
@ -448,6 +448,113 @@ describe('Permissions', function () {
.catch(done); .catch(done);
}); });
}); });
describe('App-based permissions (requires user as well)', function () {
// @TODO: revisit this - do we really need to have USER permissions AND app permissions?
it('No permissions: cannot edit tag with app only (no permissible function on model)', function (done) {
var appProviderStub = sinon.stub(providers, 'app').callsFake(function () {
// Fake the response from providers.app, which contains an empty array for this case
return Promise.resolve([]);
});
permissions
.canThis({app: {}}) // app context
.edit
.tag({id: 1}) // tag id in model syntax
.then(function () {
done(new Error('was able to edit tag without permission'));
})
.catch(function (err) {
appProviderStub.callCount.should.eql(1);
err.errorType.should.eql('NoPermissionError');
done();
});
});
it('No permissions: cannot edit tag (no permissible function on model)', function (done) {
var appProviderStub = sinon.stub(providers, 'app').callsFake(function () {
// Fake the response from providers.app, which contains an empty array for this case
return Promise.resolve([]);
}),
userProviderStub = sinon.stub(providers, 'user').callsFake(function () {
// Fake the response from providers.user, which contains permissions and roles
return Promise.resolve({
permissions: [],
roles: undefined
});
});
permissions
.canThis({app: {}, user: {}}) // app context
.edit
.tag({id: 1}) // tag id in model syntax
.then(function () {
done(new Error('was able to edit tag without permission'));
})
.catch(function (err) {
appProviderStub.callCount.should.eql(1);
userProviderStub.callCount.should.eql(1);
err.errorType.should.eql('NoPermissionError');
done();
});
});
it('With permissions: can edit specific tag (no permissible function on model)', function (done) {
var appProviderStub = sinon.stub(providers, 'app').callsFake(function () {
// Fake the response from providers.app, which contains permissions only
return Promise.resolve({
permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models
});
}),
userProviderStub = sinon.stub(providers, 'user').callsFake(function () {
// Fake the response from providers.user, which contains permissions and roles
return Promise.resolve({
permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models,
roles: undefined
});
});
permissions
.canThis({app: {}, user: {}}) // app context
.edit
.tag({id: 1}) // tag id in model syntax
.then(function (res) {
appProviderStub.callCount.should.eql(1);
userProviderStub.callCount.should.eql(1);
should.not.exist(res);
done();
})
.catch(done);
});
it('With permissions: can edit non-specific tag (no permissible function on model)', function (done) {
var appProviderStub = sinon.stub(providers, 'app').callsFake(function () {
// Fake the response from providers.app, which contains permissions only
return Promise.resolve({
permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models
});
}),
userProviderStub = sinon.stub(providers, 'user').callsFake(function () {
// Fake the response from providers.user, which contains permissions and roles
return Promise.resolve({
permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models,
roles: undefined
});
});
permissions
.canThis({app: {}, user: {}}) // app context
.edit
.tag() // tag id in model syntax
.then(function (res) {
appProviderStub.callCount.should.eql(1);
userProviderStub.callCount.should.eql(1);
should.not.exist(res);
done();
})
.catch(done);
});
});
}); });
describe('permissible (overridden)', function () { describe('permissible (overridden)', function () {
@ -472,7 +579,7 @@ describe('Permissions', function () {
}) })
.catch(function (err) { .catch(function (err) {
permissibleStub.callCount.should.eql(1); permissibleStub.callCount.should.eql(1);
permissibleStub.firstCall.args.should.have.lengthOf(7); permissibleStub.firstCall.args.should.have.lengthOf(8);
permissibleStub.firstCall.args[0].should.eql(1); permissibleStub.firstCall.args[0].should.eql(1);
permissibleStub.firstCall.args[1].should.eql('edit'); permissibleStub.firstCall.args[1].should.eql('edit');
@ -481,6 +588,7 @@ describe('Permissions', function () {
permissibleStub.firstCall.args[4].should.be.an.Object(); permissibleStub.firstCall.args[4].should.be.an.Object();
permissibleStub.firstCall.args[5].should.be.true(); permissibleStub.firstCall.args[5].should.be.true();
permissibleStub.firstCall.args[6].should.be.true(); permissibleStub.firstCall.args[6].should.be.true();
permissibleStub.firstCall.args[7].should.be.true();
userProviderStub.callCount.should.eql(1); userProviderStub.callCount.should.eql(1);
err.message.should.eql('Hello World!'); err.message.should.eql('Hello World!');
@ -506,7 +614,7 @@ describe('Permissions', function () {
.post({id: 1}) // tag id in model syntax .post({id: 1}) // tag id in model syntax
.then(function (res) { .then(function (res) {
permissibleStub.callCount.should.eql(1); permissibleStub.callCount.should.eql(1);
permissibleStub.firstCall.args.should.have.lengthOf(7); permissibleStub.firstCall.args.should.have.lengthOf(8);
permissibleStub.firstCall.args[0].should.eql(1); permissibleStub.firstCall.args[0].should.eql(1);
permissibleStub.firstCall.args[1].should.eql('edit'); permissibleStub.firstCall.args[1].should.eql('edit');
permissibleStub.firstCall.args[2].should.be.an.Object(); permissibleStub.firstCall.args[2].should.be.an.Object();
@ -514,6 +622,7 @@ describe('Permissions', function () {
permissibleStub.firstCall.args[4].should.be.an.Object(); permissibleStub.firstCall.args[4].should.be.an.Object();
permissibleStub.firstCall.args[5].should.be.true(); permissibleStub.firstCall.args[5].should.be.true();
permissibleStub.firstCall.args[6].should.be.true(); permissibleStub.firstCall.args[6].should.be.true();
permissibleStub.firstCall.args[7].should.be.true();
userProviderStub.callCount.should.eql(1); userProviderStub.callCount.should.eql(1);
should.not.exist(res); should.not.exist(res);

View File

@ -9,6 +9,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: true, public: true,
integration: null integration: null
}); });
@ -17,6 +18,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: true, public: true,
integration: null integration: null
}); });
@ -28,6 +30,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: true, public: true,
integration: null integration: null
}); });
@ -36,6 +39,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: true, public: true,
integration: null integration: null
}); });
@ -47,6 +51,7 @@ describe('Permissions', function () {
external: false, external: false,
user: 1, user: 1,
api_key: null, api_key: null,
app: null,
public: false, public: false,
integration: null integration: null
}); });
@ -64,6 +69,7 @@ describe('Permissions', function () {
id: 1, id: 1,
type: 'content' type: 'content'
}, },
app: null,
public: true, public: true,
integration: {id: 2} integration: {id: 2}
}); });
@ -81,17 +87,31 @@ describe('Permissions', function () {
id: 1, id: 1,
type: 'admin' type: 'admin'
}, },
app: null,
public: false, public: false,
integration: {id: 3} integration: {id: 3}
}); });
}); });
it('should return app if app populated', function () {
parseContext({app: 5}).should.eql({
internal: false,
external: false,
user: null,
api_key: null,
app: 5,
public: false,
integration: null
});
});
it('should return internal if internal provided', function () { it('should return internal if internal provided', function () {
parseContext({internal: true}).should.eql({ parseContext({internal: true}).should.eql({
internal: true, internal: true,
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: false, public: false,
integration: null integration: null
}); });
@ -101,6 +121,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: false, public: false,
integration: null integration: null
}); });
@ -112,6 +133,7 @@ describe('Permissions', function () {
external: true, external: true,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: false, public: false,
integration: null integration: null
}); });
@ -121,6 +143,7 @@ describe('Permissions', function () {
external: true, external: true,
user: null, user: null,
api_key: null, api_key: null,
app: null,
public: false, public: false,
integration: null integration: null
}); });

View File

@ -212,4 +212,60 @@ describe('Permission Providers', function () {
}).catch(done); }).catch(done);
}); });
}); });
describe('App', function () {
// @TODO make this consistent or sane or something!
// Why is this an empty array, when the success is an object?
// Also why is this an empty array when for users we error?!
it('returns empty array if app cannot be found!', function (done) {
var findAppSpy = sinon.stub(models.App, 'findOne').callsFake(function () {
return Promise.resolve();
});
providers.app('test')
.then(function (res) {
findAppSpy.callCount.should.eql(1);
res.should.be.an.Array().with.lengthOf(0);
done();
})
.catch(done);
});
it('can load user with role, and permissions', function (done) {
// This test requires quite a lot of unique setup work
var findAppSpy = sinon.stub(models.App, 'findOne').callsFake(function () {
var fakeApp = models.App.forge(testUtils.DataGenerator.Content.apps[0]),
fakePermissions = models.Permissions.forge(testUtils.DataGenerator.Content.permissions);
// ## Fake the relations
fakeApp.relations = {
permissions: fakePermissions
};
fakeApp.include = ['permissions'];
return Promise.resolve(fakeApp);
});
// Get permissions for the app
providers.app('kudos')
.then(function (res) {
findAppSpy.callCount.should.eql(1);
res.should.be.an.Object().with.properties('permissions');
res.permissions.should.be.an.Array().with.lengthOf(10);
should.not.exist(res.roles);
// @TODO fix this!
// Permissions is an array of models
// Roles is a JSON array
res.permissions[0].should.be.an.Object().with.properties('attributes', 'id');
res.permissions[0].should.be.instanceOf(models.Base.Model);
done();
})
.catch(done);
});
});
}); });

View File

@ -13,8 +13,9 @@ describe('Permissions', function () {
}); });
it('should return unchanged object for non-public context', function (done) { it('should return unchanged object for non-public context', function (done) {
const internal = {context: 'internal'}; var internal = {context: 'internal'},
const user = {context: {user: 1}}; user = {context: {user: 1}},
app = {context: {app: 1}};
applyPublicRules('posts', 'browse', _.cloneDeep(internal)).then(function (result) { applyPublicRules('posts', 'browse', _.cloneDeep(internal)).then(function (result) {
result.should.eql(internal); result.should.eql(internal);
@ -23,6 +24,10 @@ describe('Permissions', function () {
}).then(function (result) { }).then(function (result) {
result.should.eql(user); result.should.eql(user);
return applyPublicRules('posts', 'browse', _.cloneDeep(app));
}).then(function (result) {
result.should.eql(app);
done(); done();
}).catch(done); }).catch(done);
}); });

View File

@ -0,0 +1,14 @@
function BadApp(app) {
this.app = app;
}
BadApp.prototype.install = function () {
var knex = require('knex');
return knex.dropTableIfExists('users');
};
BadApp.prototype.activate = function () {
};
module.exports = BadApp;

View File

@ -0,0 +1,5 @@
var knex = require('knex');
module.exports = {
knex: knex
};

View File

@ -0,0 +1,14 @@
var lib = require('../example');
function BadApp(app) {
this.app = app;
}
BadApp.prototype.install = function () {
return lib.answer;
};
BadApp.prototype.activate = function () {
};
module.exports = BadApp;

View File

@ -0,0 +1,14 @@
var lib = require('./badlib');
function BadApp(app) {
this.app = app;
}
BadApp.prototype.install = function () {
return lib.knex.dropTableIfExists('users');
};
BadApp.prototype.activate = function () {
};
module.exports = BadApp;

View File

@ -0,0 +1,14 @@
var knex = require('knex');
function BadApp(app) {
this.app = app;
}
BadApp.prototype.install = function () {
return knex.dropTableIfExists('users');
};
BadApp.prototype.activate = function () {
};
module.exports = BadApp;

View File

@ -0,0 +1,22 @@
var path = require('path'),
util = require('./goodlib.js'),
nested = require('./nested/goodnested');
function GoodApp(app) {
this.app = app;
}
GoodApp.prototype.install = function () {
// Goes through app to do data
this.app.something = 42;
this.app.util = util;
this.app.nested = nested;
this.app.path = path.join(__dirname, 'good.js');
return true;
};
GoodApp.prototype.activate = function () {
};
module.exports = GoodApp;

View File

@ -0,0 +1,5 @@
module.exports = {
util: function () {
return 42;
}
};

View File

@ -0,0 +1,5 @@
var lib = require('../goodlib.js');
module.exports = {
other: 42
};

View File

@ -296,6 +296,60 @@ DataGenerator.Content = {
} }
], ],
apps: [
{
id: ObjectId.generate(),
name: 'Kudos',
slug: 'kudos',
version: '0.0.1',
status: 'installed'
},
{
id: ObjectId.generate(),
name: 'Importer',
slug: 'importer',
version: '0.1.0',
status: 'inactive'
},
{
id: ObjectId.generate(),
name: 'Hemingway',
slug: 'hemingway',
version: '1.0.0',
status: 'installed'
}
],
app_fields: [
{
id: ObjectId.generate(),
key: 'count',
value: '120',
type: 'number',
active: true
},
{
id: ObjectId.generate(),
key: 'words',
value: '512',
type: 'number',
active: true
}
],
app_settings: [
{
id: ObjectId.generate(),
key: 'color',
value: 'ghosty'
},
{
id: ObjectId.generate(),
key: 'setting',
value: 'value'
}
],
subscribers: [ subscribers: [
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
@ -565,6 +619,40 @@ DataGenerator.forKnex = (function () {
}; };
} }
function createAppField(overrides) {
var newObj = _.cloneDeep(overrides);
return _.defaults(newObj, {
id: ObjectId.generate(),
created_by: DataGenerator.Content.users[0].id,
created_at: new Date(),
active: true,
app_id: DataGenerator.Content.apps[0].id,
relatable_id: DataGenerator.Content.posts[0].id,
relatable_type: 'posts'
});
}
function createAppSetting(overrides) {
var newObj = _.cloneDeep(overrides);
return _.defaults(newObj, {
id: ObjectId.generate(),
app_id: DataGenerator.Content.apps[0].id,
created_by: DataGenerator.Content.users[0].id,
created_at: new Date()
});
}
function createSubscriber(overrides) {
const newObj = _.cloneDeep(overrides);
return _.defaults(newObj, {
id: ObjectId.generate(),
email: 'subscriber@ghost.org'
});
}
function createMember(overrides) { function createMember(overrides) {
const newObj = _.cloneDeep(overrides); const newObj = _.cloneDeep(overrides);
@ -811,6 +899,17 @@ DataGenerator.forKnex = (function () {
} }
]; ];
const apps = [
createBasic(DataGenerator.Content.apps[0]),
createBasic(DataGenerator.Content.apps[1]),
createBasic(DataGenerator.Content.apps[2])
];
const app_fields = [
createAppField(DataGenerator.Content.app_fields[0]),
createAppField(DataGenerator.Content.app_fields[1])
];
const invites = [ const invites = [
createInvite({email: 'test1@ghost.org', role_id: DataGenerator.Content.roles[0].id}), createInvite({email: 'test1@ghost.org', role_id: DataGenerator.Content.roles[0].id}),
createInvite({email: 'test2@ghost.org', role_id: DataGenerator.Content.roles[2].id}) createInvite({email: 'test2@ghost.org', role_id: DataGenerator.Content.roles[2].id})
@ -850,8 +949,12 @@ DataGenerator.forKnex = (function () {
createRole: createBasic, createRole: createBasic,
createPermission: createBasic, createPermission: createBasic,
createPostsTags: createPostsTags, createPostsTags: createPostsTags,
createApp: createBasic,
createAppField: createAppField,
createSetting: createSetting, createSetting: createSetting,
createAppSetting: createAppSetting,
createToken: createToken, createToken: createToken,
createSubscriber: createSubscriber,
createMember: createMember, createMember: createMember,
createInvite: createInvite, createInvite: createInvite,
createWebhook: createWebhook, createWebhook: createWebhook,
@ -862,6 +965,8 @@ DataGenerator.forKnex = (function () {
tags: tags, tags: tags,
posts_tags: posts_tags, posts_tags: posts_tags,
posts_authors: posts_authors, posts_authors: posts_authors,
apps: apps,
app_fields: app_fields,
roles: roles, roles: roles,
users: users, users: users,
roles_users: roles_users, roles_users: roles_users,

View File

@ -523,6 +523,21 @@ clearData = function clearData() {
}; };
toDoList = { toDoList = {
app: function insertApp() {
return fixtures.insertOne('App', 'apps', 'createApp');
},
app_field: function insertAppField() {
// TODO: use the actual app ID to create the field
return fixtures.insertOne('App', 'apps', 'createApp').then(function () {
return fixtures.insertOne('AppField', 'app_fields', 'createAppField');
});
},
app_setting: function insertAppSetting() {
// TODO: use the actual app ID to create the field
return fixtures.insertOne('App', 'apps', 'createApp').then(function () {
return fixtures.insertOne('AppSetting', 'app_settings', 'createAppSetting');
});
},
permission: function insertPermission() { permission: function insertPermission() {
return fixtures.insertOne('Permission', 'permissions', 'createPermission'); return fixtures.insertOne('Permission', 'permissions', 'createPermission');
}, },
@ -535,6 +550,9 @@ toDoList = {
tag: function insertTag() { tag: function insertTag() {
return fixtures.insertOne('Tag', 'tags', 'createTag'); return fixtures.insertOne('Tag', 'tags', 'createTag');
}, },
subscriber: function insertSubscriber() {
return fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
},
member: function insertMember() { member: function insertMember() {
return fixtures.insertOne('Member', 'members', 'createMember'); return fixtures.insertOne('Member', 'members', 'createMember');
}, },
@ -550,6 +568,9 @@ toDoList = {
'tags:extra': function insertExtraTags() { 'tags:extra': function insertExtraTags() {
return fixtures.insertExtraTags(); return fixtures.insertExtraTags();
}, },
apps: function insertApps() {
return fixtures.insertApps();
},
settings: function populateSettings() { settings: function populateSettings() {
settingsCache.shutdown(); settingsCache.shutdown();
return settingsService.init(); return settingsService.init();
@ -1013,6 +1034,10 @@ module.exports = {
cacheStub.withArgs('amp').returns(true); cacheStub.withArgs('amp').returns(true);
} }
if (options.apps) {
cacheStub.withArgs('active_apps').returns([]);
}
sandbox.stub(imageLib.imageSize, 'getImageSizeFromUrl').resolves(); sandbox.stub(imageLib.imageSize, 'getImageSizeFromUrl').resolves();
}, },