mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 05:14:12 +03:00
parent
cbb59a57db
commit
bc7906a7b2
@ -1,6 +1,6 @@
|
||||
// This file defines everything that helpers "require"
|
||||
// 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'),
|
||||
settingsCache = require('../../server/services/settings/cache'),
|
||||
config = require('../../server/config');
|
||||
|
2
core/frontend/services/routing/bootstrap.js
vendored
2
core/frontend/services/routing/bootstrap.js
vendored
@ -58,7 +58,7 @@ module.exports.init = (options = {start: false}) => {
|
||||
* 3. Taxonomies: Stronger than collections, because it's an inbuilt feature.
|
||||
* 4. Collections
|
||||
* 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) => {
|
||||
const RESOURCE_CONFIG = require(`./config/${apiVersion}`);
|
||||
|
@ -4,7 +4,8 @@ const common = require('../../lib/common');
|
||||
const allowedTypes = {
|
||||
post: models.Post,
|
||||
tag: models.Tag,
|
||||
user: models.User
|
||||
user: models.User,
|
||||
app: models.App
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -4,7 +4,8 @@ const common = require('../../lib/common');
|
||||
const allowedTypes = {
|
||||
post: models.Post,
|
||||
tag: models.Tag,
|
||||
user: models.User
|
||||
user: models.User,
|
||||
app: models.App
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -1,11 +1,10 @@
|
||||
const debug = require('ghost-ignition').debug('importer:settings');
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const BaseImporter = require('./base');
|
||||
const models = require('../../../../models');
|
||||
const defaultSettings = require('../../../schema').defaultSettings;
|
||||
const labsDefaults = JSON.parse(defaultSettings.blog.labs.defaultValue);
|
||||
const deprecatedSettings = ['active_apps', 'installed_apps'];
|
||||
const debug = require('ghost-ignition').debug('importer:settings'),
|
||||
Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
BaseImporter = require('./base'),
|
||||
models = require('../../../../models'),
|
||||
defaultSettings = require('../../../schema').defaultSettings,
|
||||
labsDefaults = JSON.parse(defaultSettings.blog.labs.defaultValue);
|
||||
|
||||
const isFalse = (value) => {
|
||||
// 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) => {
|
||||
return !_.includes(deprecatedSettings, data.key);
|
||||
return data.key !== 'active_apps' && data.key !== 'installed_apps';
|
||||
});
|
||||
|
||||
const permalinks = _.find(this.dataToImport, {key: 'permalinks'});
|
||||
|
@ -152,7 +152,7 @@ module.exports = {
|
||||
maxlength: 50,
|
||||
nullable: false,
|
||||
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_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
|
@ -30,6 +30,7 @@ function initialiseServices() {
|
||||
routing.bootstrap.start(themeService.getApiVersion());
|
||||
|
||||
const permissions = require('./services/permissions'),
|
||||
apps = require('./services/apps'),
|
||||
xmlrpc = require('./services/xmlrpc'),
|
||||
slack = require('./services/slack'),
|
||||
{mega} = require('./services/mega'),
|
||||
@ -45,6 +46,7 @@ function initialiseServices() {
|
||||
slack.listen(),
|
||||
mega.listen(),
|
||||
webhooks.listen(),
|
||||
apps.init(),
|
||||
scheduling.init({
|
||||
schedulerUrl: config.get('scheduling').schedulerUrl,
|
||||
active: config.get('scheduling').active,
|
||||
@ -55,7 +57,7 @@ function initialiseServices() {
|
||||
contentPath: config.getContentPath('scheduling')
|
||||
})
|
||||
).then(function () {
|
||||
debug('XMLRPC, Slack, MEGA, Webhooks, Scheduling, Permissions done');
|
||||
debug('XMLRPC, Slack, MEGA, Webhooks, Apps, Scheduling, Permissions done');
|
||||
|
||||
// Initialise analytics events
|
||||
if (config.get('segment:key')) {
|
||||
|
@ -4,7 +4,7 @@ var _ = require('lodash'),
|
||||
|
||||
/**
|
||||
* ### 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
|
||||
* with 3 properties:
|
||||
* - `name` - the package name
|
||||
@ -17,10 +17,10 @@ var _ = require('lodash'),
|
||||
*
|
||||
* @param {object} packages as returned by read-packages
|
||||
* @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) {
|
||||
// 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)) {
|
||||
active = [active];
|
||||
}
|
||||
|
@ -3,7 +3,9 @@
|
||||
*
|
||||
* 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
|
||||
* - 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.
|
||||
*/
|
||||
|
20
core/server/models/app-field.js
Normal file
20
core/server/models/app-field.js
Normal 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)
|
||||
};
|
20
core/server/models/app-setting.js
Normal file
20
core/server/models/app-setting.js
Normal 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
60
core/server/models/app.js
Normal 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)
|
||||
};
|
@ -3,9 +3,8 @@
|
||||
// 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
|
||||
// accesses the models directly.
|
||||
|
||||
// All other parts of Ghost, including the frontend & admin UI are only allowed to access data via the API.
|
||||
// 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.
|
||||
const _ = require('lodash'),
|
||||
bookshelf = require('bookshelf'),
|
||||
moment = require('moment'),
|
||||
|
@ -15,6 +15,9 @@ require('./base/listeners');
|
||||
exports = module.exports;
|
||||
|
||||
models = [
|
||||
'app-field',
|
||||
'app-setting',
|
||||
'app',
|
||||
'permission',
|
||||
'post',
|
||||
'role',
|
||||
|
@ -42,11 +42,11 @@ Invite = ghostBookshelf.Model.extend({
|
||||
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');
|
||||
|
||||
if (!isAdd) {
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ Invite = ghostBookshelf.Model.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,10 @@ Permission = ghostBookshelf.Model.extend({
|
||||
|
||||
users: function users() {
|
||||
return this.belongsToMany('User');
|
||||
},
|
||||
|
||||
apps: function apps() {
|
||||
return this.belongsToMany('App');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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.
|
||||
permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
|
||||
permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
|
||||
let isContributor;
|
||||
let isOwner;
|
||||
let isAdmin;
|
||||
@ -989,7 +989,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
excludedAttrs.push('tags');
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve({excludedAttrs});
|
||||
}
|
||||
|
||||
|
@ -331,7 +331,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
|
||||
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,
|
||||
postModel = postModelOrId,
|
||||
origArgs, isContributor, isAuthor, isEdit, isAdd, isDestroy;
|
||||
@ -420,7 +420,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
|
||||
hasUserPermission = hasUserPermission || isPrimaryAuthor();
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Post.permissible.call(
|
||||
this,
|
||||
postModelOrId,
|
||||
@ -428,6 +428,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
|
||||
unsafeAttrs,
|
||||
loadedPermissions,
|
||||
hasUserPermission,
|
||||
hasAppPermission,
|
||||
hasApiKeyPermission
|
||||
).then(({excludedAttrs}) => {
|
||||
// @TODO: we need a concept for making a diff between incoming authors and existing authors
|
||||
|
@ -50,7 +50,7 @@ Role = ghostBookshelf.Model.extend({
|
||||
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
|
||||
// then check the permissions
|
||||
if (_.isNumber(roleModelOrId) || _.isString(roleModelOrId)) {
|
||||
@ -95,7 +95,7 @@ Role = ghostBookshelf.Model.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasAppPermission && hasApiKeyPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -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 isOwner;
|
||||
|
||||
@ -271,7 +271,7 @@ Settings = ghostBookshelf.Model.extend({
|
||||
hasUserPermission = isOwner;
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
userModel = userModelOrId,
|
||||
origArgs;
|
||||
@ -738,7 +738,7 @@ User = ghostBookshelf.Model.extend({
|
||||
.then((owner) => {
|
||||
// CASE: owner can assign role to any user
|
||||
if (context.user === owner.id) {
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -760,7 +760,7 @@ User = ghostBookshelf.Model.extend({
|
||||
// e.g. admin can assign admin role to a user, but not owner
|
||||
return permissions.canThis(context).assign.role(role)
|
||||
.then(() => {
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -770,7 +770,7 @@ User = ghostBookshelf.Model.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -780,7 +780,7 @@ User = ghostBookshelf.Model.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasApiKeyPermission) {
|
||||
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
21
core/server/services/apps/index.js
Normal file
21
core/server/services/apps/index.js
Normal 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')
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
45
core/server/services/apps/loader.js
Normal file
45
core/server/services/apps/loader.js
Normal 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);
|
||||
}
|
||||
};
|
18
core/server/services/apps/proxy.js
Normal file
18
core/server/services/apps/proxy.js
Normal 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)
|
||||
}
|
||||
};
|
||||
};
|
@ -50,8 +50,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
|
||||
// Iterate through the user permissions looking for an affirmation
|
||||
var userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null,
|
||||
apiKeyPermissions = loadedPermissions.apiKey ? loadedPermissions.apiKey.permissions : null,
|
||||
appPermissions = loadedPermissions.app ? loadedPermissions.app.permissions : null,
|
||||
hasUserPermission,
|
||||
hasApiKeyPermission,
|
||||
hasAppPermission,
|
||||
checkPermission = function (perm) {
|
||||
var permObjId;
|
||||
|
||||
@ -89,14 +91,20 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
|
||||
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
|
||||
if (TargetModel && _.isFunction(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;
|
||||
}
|
||||
|
||||
@ -112,6 +120,7 @@ CanThisResult.prototype.beginCheck = function (context) {
|
||||
var self = this,
|
||||
userPermissionLoad,
|
||||
apiKeyPermissionLoad,
|
||||
appPermissionLoad,
|
||||
permissionsLoad;
|
||||
|
||||
// Get context.user, context.api_key and context.app
|
||||
@ -137,11 +146,20 @@ CanThisResult.prototype.beginCheck = function (context) {
|
||||
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
|
||||
permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad]).then(function (result) {
|
||||
permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad, appPermissionLoad]).then(function (result) {
|
||||
return {
|
||||
user: result[0],
|
||||
apiKey: result[1]
|
||||
apiKey: result[1],
|
||||
app: result[2]
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -3,14 +3,16 @@
|
||||
*
|
||||
* Utility function, to expand strings out into objects.
|
||||
* @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) {
|
||||
// Parse what's passed to canThis.beginCheck for standard user and app scopes
|
||||
var parsed = {
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
integration: null,
|
||||
public: true
|
||||
};
|
||||
@ -37,5 +39,10 @@ module.exports = function parseContext(context) {
|
||||
parsed.public = (context.api_key.type === 'content');
|
||||
}
|
||||
|
||||
if (context && context.app) {
|
||||
parsed.app = context.app;
|
||||
parsed.public = false;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
@ -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) {
|
||||
return models.ApiKey.findOne({id}, {withRelated: ['role', 'role.permissions']})
|
||||
.then((foundApiKey) => {
|
||||
|
@ -34,6 +34,18 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"api": {
|
||||
"versionMismatch": "Client request for {clientVersion} does not match server version {serverVersion}."
|
||||
|
@ -47,7 +47,7 @@ module.exports = function setupParentApp(options = {}) {
|
||||
// This sets global res.locals which are needed everywhere
|
||||
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 frontendHost = new URL(config.get('url')).hostname;
|
||||
|
@ -7,6 +7,7 @@ const common = require('../../lib/common');
|
||||
|
||||
// App requires
|
||||
const config = require('../../config');
|
||||
const apps = require('../../services/apps');
|
||||
const constants = require('../../lib/constants');
|
||||
const storage = require('../../adapters/storage');
|
||||
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));
|
||||
|
||||
// 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) => {
|
||||
const app = require(path.join(config.get('paths').internalAppPath, appName));
|
||||
|
||||
@ -210,6 +211,9 @@ module.exports.reload = () => {
|
||||
router = siteRoutes({start: themeService.getApiVersion()});
|
||||
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
|
||||
urlService.queue.start({
|
||||
event: 'init',
|
||||
|
@ -5,6 +5,7 @@ const should = require('should'),
|
||||
testUtils = require('../../utils'),
|
||||
configUtils = require('../../utils/configUtils'),
|
||||
urlUtils = require('../../utils/urlUtils'),
|
||||
appsService = require('../../../server/services/apps'),
|
||||
frontendSettingsService = require('../../../frontend/services/settings'),
|
||||
themeService = require('../../../frontend/services/themes'),
|
||||
siteApp = require('../../../server/web/parent-app');
|
||||
@ -22,7 +23,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('default routes.yaml', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
return testUtils.integrationTesting.initGhost()
|
||||
@ -32,6 +33,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1717,7 +1721,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('default routes.yaml', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
return testUtils.integrationTesting.initGhost()
|
||||
@ -1727,6 +1731,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
@ -3414,7 +3421,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('default routes.yaml', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
return testUtils.integrationTesting.initGhost()
|
||||
@ -3424,6 +3431,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5110,7 +5120,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('no separate admin', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
configUtils.set('url', 'http://example.com');
|
||||
@ -5123,6 +5133,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5226,7 +5239,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('separate admin host', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
configUtils.set('url', 'http://example.com');
|
||||
@ -5239,6 +5252,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
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 () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
configUtils.set('url', 'http://example.com');
|
||||
@ -5398,6 +5414,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5429,7 +5448,7 @@ describe('Integration - Web - Site', function () {
|
||||
describe('same host separate protocol', function () {
|
||||
before(function () {
|
||||
testUtils.integrationTesting.urlService.resetGenerators();
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true});
|
||||
testUtils.integrationTesting.defaultMocks(sinon, {amp: true, apps: true});
|
||||
testUtils.integrationTesting.overrideGhostConfig(configUtils);
|
||||
|
||||
configUtils.set('url', 'http://example.com');
|
||||
@ -5442,6 +5461,9 @@ describe('Integration - Web - Site', function () {
|
||||
|
||||
app = siteApp({start: true});
|
||||
return testUtils.integrationTesting.urlService.waitTillFinished();
|
||||
})
|
||||
.then(() => {
|
||||
return appsService.init();
|
||||
});
|
||||
});
|
||||
|
||||
|
36
core/test/unit/services/apps/proxy_spec.js
Normal file
36
core/test/unit/services/apps/proxy_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -99,7 +99,7 @@ describe('Permissions', 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!
|
||||
// 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.
|
||||
@ -448,6 +448,113 @@ describe('Permissions', function () {
|
||||
.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 () {
|
||||
@ -472,7 +579,7 @@ describe('Permissions', function () {
|
||||
})
|
||||
.catch(function (err) {
|
||||
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[1].should.eql('edit');
|
||||
@ -481,6 +588,7 @@ describe('Permissions', function () {
|
||||
permissibleStub.firstCall.args[4].should.be.an.Object();
|
||||
permissibleStub.firstCall.args[5].should.be.true();
|
||||
permissibleStub.firstCall.args[6].should.be.true();
|
||||
permissibleStub.firstCall.args[7].should.be.true();
|
||||
|
||||
userProviderStub.callCount.should.eql(1);
|
||||
err.message.should.eql('Hello World!');
|
||||
@ -506,7 +614,7 @@ describe('Permissions', function () {
|
||||
.post({id: 1}) // tag id in model syntax
|
||||
.then(function (res) {
|
||||
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[1].should.eql('edit');
|
||||
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[5].should.be.true();
|
||||
permissibleStub.firstCall.args[6].should.be.true();
|
||||
permissibleStub.firstCall.args[7].should.be.true();
|
||||
|
||||
userProviderStub.callCount.should.eql(1);
|
||||
should.not.exist(res);
|
||||
|
@ -9,6 +9,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: true,
|
||||
integration: null
|
||||
});
|
||||
@ -17,6 +18,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: true,
|
||||
integration: null
|
||||
});
|
||||
@ -28,6 +30,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: true,
|
||||
integration: null
|
||||
});
|
||||
@ -36,6 +39,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: true,
|
||||
integration: null
|
||||
});
|
||||
@ -47,6 +51,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: 1,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: false,
|
||||
integration: null
|
||||
});
|
||||
@ -64,6 +69,7 @@ describe('Permissions', function () {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
},
|
||||
app: null,
|
||||
public: true,
|
||||
integration: {id: 2}
|
||||
});
|
||||
@ -81,17 +87,31 @@ describe('Permissions', function () {
|
||||
id: 1,
|
||||
type: 'admin'
|
||||
},
|
||||
app: null,
|
||||
public: false,
|
||||
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 () {
|
||||
parseContext({internal: true}).should.eql({
|
||||
internal: true,
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: false,
|
||||
integration: null
|
||||
});
|
||||
@ -101,6 +121,7 @@ describe('Permissions', function () {
|
||||
external: false,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: false,
|
||||
integration: null
|
||||
});
|
||||
@ -112,6 +133,7 @@ describe('Permissions', function () {
|
||||
external: true,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: false,
|
||||
integration: null
|
||||
});
|
||||
@ -121,6 +143,7 @@ describe('Permissions', function () {
|
||||
external: true,
|
||||
user: null,
|
||||
api_key: null,
|
||||
app: null,
|
||||
public: false,
|
||||
integration: null
|
||||
});
|
||||
|
@ -212,4 +212,60 @@ describe('Permission Providers', function () {
|
||||
}).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -13,8 +13,9 @@ describe('Permissions', function () {
|
||||
});
|
||||
|
||||
it('should return unchanged object for non-public context', function (done) {
|
||||
const internal = {context: 'internal'};
|
||||
const user = {context: {user: 1}};
|
||||
var internal = {context: 'internal'},
|
||||
user = {context: {user: 1}},
|
||||
app = {context: {app: 1}};
|
||||
|
||||
applyPublicRules('posts', 'browse', _.cloneDeep(internal)).then(function (result) {
|
||||
result.should.eql(internal);
|
||||
@ -23,6 +24,10 @@ describe('Permissions', function () {
|
||||
}).then(function (result) {
|
||||
result.should.eql(user);
|
||||
|
||||
return applyPublicRules('posts', 'browse', _.cloneDeep(app));
|
||||
}).then(function (result) {
|
||||
result.should.eql(app);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
14
core/test/utils/fixtures/app/badinstall.js
Normal file
14
core/test/utils/fixtures/app/badinstall.js
Normal 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;
|
5
core/test/utils/fixtures/app/badlib.js
Normal file
5
core/test/utils/fixtures/app/badlib.js
Normal file
@ -0,0 +1,5 @@
|
||||
var knex = require('knex');
|
||||
|
||||
module.exports = {
|
||||
knex: knex
|
||||
};
|
14
core/test/utils/fixtures/app/badoutside.js
Normal file
14
core/test/utils/fixtures/app/badoutside.js
Normal 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;
|
14
core/test/utils/fixtures/app/badrequire.js
Normal file
14
core/test/utils/fixtures/app/badrequire.js
Normal 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;
|
14
core/test/utils/fixtures/app/badtop.js
Normal file
14
core/test/utils/fixtures/app/badtop.js
Normal 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;
|
22
core/test/utils/fixtures/app/good.js
Normal file
22
core/test/utils/fixtures/app/good.js
Normal 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;
|
5
core/test/utils/fixtures/app/goodlib.js
Normal file
5
core/test/utils/fixtures/app/goodlib.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
util: function () {
|
||||
return 42;
|
||||
}
|
||||
};
|
5
core/test/utils/fixtures/app/nested/goodnested.js
Normal file
5
core/test/utils/fixtures/app/nested/goodnested.js
Normal file
@ -0,0 +1,5 @@
|
||||
var lib = require('../goodlib.js');
|
||||
|
||||
module.exports = {
|
||||
other: 42
|
||||
};
|
@ -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: [
|
||||
{
|
||||
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) {
|
||||
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 = [
|
||||
createInvite({email: 'test1@ghost.org', role_id: DataGenerator.Content.roles[0].id}),
|
||||
createInvite({email: 'test2@ghost.org', role_id: DataGenerator.Content.roles[2].id})
|
||||
@ -850,8 +949,12 @@ DataGenerator.forKnex = (function () {
|
||||
createRole: createBasic,
|
||||
createPermission: createBasic,
|
||||
createPostsTags: createPostsTags,
|
||||
createApp: createBasic,
|
||||
createAppField: createAppField,
|
||||
createSetting: createSetting,
|
||||
createAppSetting: createAppSetting,
|
||||
createToken: createToken,
|
||||
createSubscriber: createSubscriber,
|
||||
createMember: createMember,
|
||||
createInvite: createInvite,
|
||||
createWebhook: createWebhook,
|
||||
@ -862,6 +965,8 @@ DataGenerator.forKnex = (function () {
|
||||
tags: tags,
|
||||
posts_tags: posts_tags,
|
||||
posts_authors: posts_authors,
|
||||
apps: apps,
|
||||
app_fields: app_fields,
|
||||
roles: roles,
|
||||
users: users,
|
||||
roles_users: roles_users,
|
||||
|
@ -523,6 +523,21 @@ clearData = function clearData() {
|
||||
};
|
||||
|
||||
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() {
|
||||
return fixtures.insertOne('Permission', 'permissions', 'createPermission');
|
||||
},
|
||||
@ -535,6 +550,9 @@ toDoList = {
|
||||
tag: function insertTag() {
|
||||
return fixtures.insertOne('Tag', 'tags', 'createTag');
|
||||
},
|
||||
subscriber: function insertSubscriber() {
|
||||
return fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
|
||||
},
|
||||
member: function insertMember() {
|
||||
return fixtures.insertOne('Member', 'members', 'createMember');
|
||||
},
|
||||
@ -550,6 +568,9 @@ toDoList = {
|
||||
'tags:extra': function insertExtraTags() {
|
||||
return fixtures.insertExtraTags();
|
||||
},
|
||||
apps: function insertApps() {
|
||||
return fixtures.insertApps();
|
||||
},
|
||||
settings: function populateSettings() {
|
||||
settingsCache.shutdown();
|
||||
return settingsService.init();
|
||||
@ -1013,6 +1034,10 @@ module.exports = {
|
||||
cacheStub.withArgs('amp').returns(true);
|
||||
}
|
||||
|
||||
if (options.apps) {
|
||||
cacheStub.withArgs('active_apps').returns([]);
|
||||
}
|
||||
|
||||
sandbox.stub(imageLib.imageSize, 'getImageSizeFromUrl').resolves();
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user