diff --git a/core/server/data/migrations/versions/2.2/2-add-integrations-and-api-key-tables.js b/core/server/data/migrations/versions/2.2/2-add-integrations-and-api-key-tables.js new file mode 100644 index 0000000000..a3d469f8c3 --- /dev/null +++ b/core/server/data/migrations/versions/2.2/2-add-integrations-and-api-key-tables.js @@ -0,0 +1,33 @@ +const commands = require('../../../schema').commands; +const logging = require('../../../../lib/common/logging'); + +const tables = ['integrations', 'api_keys']; +const _private = {}; + +_private.addOrRemoveTable = (type, table, options) => { + const isAdding = type === 'Adding'; + const operation = isAdding ? commands.createTable : commands.deleteTable; + const message = `${type} ${table} table`; + + return options.connection.schema.hasTable(table) + .then((exists) => { + if ((isAdding && exists || !isAdding && !exists)) { + logging.warn(message); + return Promise.resolve(); + } + + logging.info(message); + return operation(table, options.connection); + }); +}; + +_private.handle = (migrationOptions) => { + return (options) => { + return Promise.each(tables, (table) => { + return _private.addOrRemoveTable(migrationOptions.type, table, options); + }); + }; +}; + +module.exports.up = _private.handle({type: 'Adding'}); +module.exports.down = _private.handle({type: 'Dropping'}); diff --git a/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js b/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js new file mode 100644 index 0000000000..994badd6b5 --- /dev/null +++ b/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js @@ -0,0 +1,83 @@ +const logging = require('../../../../lib/common/logging'); +const merge = require('lodash/merge'); +const models = require('../../../../models'); +const utils = require('../../../schema/fixtures/utils'); + +const _private = {}; + +_private.printResult = function printResult(result, message) { + if (result.done === result.expected) { + logging.info(message); + } else { + logging.warn(`(${result.done}/${result.expected}) ${message}`); + } +}; + +_private.addApiKeyRole = (options) => { + const message = 'Adding "Admin Integration" role to roles table'; + const apiKeyRole = utils.findModelFixtureEntry('Role', {name: 'Admin Integration'}); + + return models.Role.findOne({name: apiKeyRole.name}, options) + .then((role) => { + if (!role) { + return utils.addFixturesForModel({ + name: 'Role', + entries: [apiKeyRole] + }, options).then(result => _private.printResult(result, message)); + } + + logging.warn(message); + }); +}; + +_private.addApiKeyPermissions = (options) => { + const message = 'Adding permissions for the "Admin Integration" role'; + const relations = utils.findRelationFixture('Role', 'Permission'); + + return utils.addFixturesForRelation({ + from: relations.from, + to: relations.to, + entries: { + 'Admin Integration': relations.entries['Admin Integration'] + } + }, options).then(result => _private.printResult(result, message)); +}; + +_private.removeApiKeyPermissionsAndRole = (options) => { + const message = 'Rollback: Removing "Admin Integration" role and permissions'; + + return models.Role.findOne({name: 'Admin Integration'}, options) + .then((role) => { + if (!role) { + logging.warn(message); + return; + } + + return role.destroy().then(() => { + logging.info(message); + }); + }); +}; + +module.exports.config = { + transaction: true +}; + +module.exports.up = (options) => { + const localOptions = merge({ + context: {internal: true}, + migrating: true + }, options); + + return _private.addApiKeyRole(localOptions) + .then(() => _private.addApiKeyPermissions(localOptions)); +}; + +module.exports.down = (options) => { + const localOptions = merge({ + context: {internal: true}, + migrating: true + }, options); + + return _private.removeApiKeyPermissionsAndRole(localOptions); +}; diff --git a/core/server/data/migrations/versions/2.2/4-insert-integration-and-api-key-permissions.js b/core/server/data/migrations/versions/2.2/4-insert-integration-and-api-key-permissions.js new file mode 100644 index 0000000000..a16a363885 --- /dev/null +++ b/core/server/data/migrations/versions/2.2/4-insert-integration-and-api-key-permissions.js @@ -0,0 +1,58 @@ +const _ = require('lodash'); +const utils = require('../../../schema/fixtures/utils'); +const permissions = require('../../../../services/permissions'); +const logging = require('../../../../lib/common/logging'); + +const resources = ['integration', 'api_key']; +const _private = {}; + +_private.getPermissions = function getPermissions(resource) { + return utils.findModelFixtures('Permission', {object_type: resource}); +}; + +_private.getRelations = function getRelations(resource) { + return utils.findPermissionRelationsForObject(resource); +}; + +_private.printResult = function printResult(result, message) { + if (result.done === result.expected) { + logging.info(message); + } else { + logging.warn(`(${result.done}/${result.expected}) ${message}`); + } +}; + +module.exports.config = { + transaction: true +}; + +module.exports.up = (options) => { + const localOptions = _.merge({ + context: {internal: true} + }, options); + + return Promise.map(resources, (resource) => { + const modelToAdd = _private.getPermissions(resource); + const relationToAdd = _private.getRelations(resource); + + return utils.addFixturesForModel(modelToAdd, localOptions) + .then(result => _private.printResult(result, `Adding permissions fixtures for ${resource}s`)) + .then(() => utils.addFixturesForRelation(relationToAdd, localOptions)) + .then(result => _private.printResult(result, `Adding permissions_roles fixtures for ${resource}s`)) + .then(() => permissions.init(localOptions)); + }); +}; + +module.exports.down = (options) => { + const localOptions = _.merge({ + context: {internal: true} + }, options); + + return Promise.map(resources, (resource) => { + const modelToRemove = _private.getPermissions(resource); + + // permission model automatically cleans up permissions_roles on .destroy() + return utils.removeFixturesForModel(modelToRemove, localOptions) + .then(result => _private.printResult(result, `Removing permissions fixtures for ${resource}s`)); + }); +}; diff --git a/core/server/data/schema/fixtures/fixtures.json b/core/server/data/schema/fixtures/fixtures.json index ac4c89ead7..25020c7c91 100644 --- a/core/server/data/schema/fixtures/fixtures.json +++ b/core/server/data/schema/fixtures/fixtures.json @@ -44,24 +44,28 @@ "name": "Role", "entries": [ { - "name": "Administrator", - "description": "Administrators" + "name": "Administrator", + "description": "Administrators" }, { - "name": "Editor", - "description": "Editors" + "name": "Editor", + "description": "Editors" }, { - "name": "Author", - "description": "Authors" + "name": "Author", + "description": "Authors" }, { - "name": "Contributor", - "description": "Contributors" + "name": "Contributor", + "description": "Contributors" }, { - "name": "Owner", - "description": "Blog Owner" + "name": "Owner", + "description": "Blog Owner" + }, + { + "name": "Admin Integration", + "description": "External Apps" } ] }, @@ -332,6 +336,56 @@ "name": "Delete webhooks", "action_type": "destroy", "object_type": "webhook" + }, + { + "name": "Browse integrations", + "action_type": "browse", + "object_type": "integration" + }, + { + "name": "Read integrations", + "action_type": "read", + "object_type": "integration" + }, + { + "name": "Edit integrations", + "action_type": "edit", + "object_type": "integration" + }, + { + "name": "Add integrations", + "action_type": "add", + "object_type": "integration" + }, + { + "name": "Delete integrations", + "action_type": "destroy", + "object_type": "integration" + }, + { + "name": "Browse API keys", + "action_type": "browse", + "object_type": "api_key" + }, + { + "name": "Read API keys", + "action_type": "read", + "object_type": "api_key" + }, + { + "name": "Edit API keys", + "action_type": "edit", + "object_type": "api_key" + }, + { + "name": "Add API keys", + "action_type": "add", + "object_type": "api_key" + }, + { + "name": "Delete API keys", + "action_type": "destroy", + "object_type": "api_key" } ] }, @@ -485,6 +539,24 @@ "entries": { "Administrator": { "db": "all", + "mail": "all", + "notification": "all", + "post": "all", + "setting": "all", + "slug": "all", + "tag": "all", + "theme": "all", + "user": "all", + "role": "all", + "client": "all", + "subscriber": "all", + "invite": "all", + "redirect": "all", + "webhook": "all", + "integration": "all", + "api_key": "all" + }, + "Admin Integration": { "mail": "all", "notification": "all", "post": "all", diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index 7d214c3d70..683b5f455c 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -308,5 +308,41 @@ module.exports = { session_data: {type: 'string', maxlength: 2000, nullable: false}, created_at: {type: 'dateTime', nullable: false}, updated_at: {type: 'dateTime', nullable: true} + }, + integrations: { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + name: {type: 'string', maxlength: 191, nullable: false}, + slug: {type: 'string', maxlength: 191, nullable: false, unique: true}, + icon_image: {type: 'string', maxlength: 2000, nullable: true}, + description: {type: 'string', maxlength: 2000, nullable: true}, + created_at: {type: 'dateTime', nullable: false}, + created_by: {type: 'string', maxlength: 24, nullable: false}, + updated_at: {type: 'dateTime', nullable: true}, + updated_by: {type: 'string', maxlength: 24, nullable: true} + }, + api_keys: { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + type: { + type: 'string', + maxlength: 50, + nullable: false, + validations: {isIn: [['content', 'admin']]} + }, + secret: { + type: 'string', + maxlength: 191, + nullable: false, + unique: true, + validations: {isLength: {min: 128, max: 128}} + }, + role_id: {type: 'string', maxlength: 24, nullable: true}, + // integration_id is nullable to allow "internal" API keys that don't show in the UI + integration_id: {type: 'string', maxlength: 24, nullable: true}, + last_seen_at: {type: 'dateTime', nullable: true}, + last_seen_version: {type: 'string', maxlength: 50, nullable: true}, + created_at: {type: 'dateTime', nullable: false}, + created_by: {type: 'string', maxlength: 24, nullable: false}, + updated_at: {type: 'dateTime', nullable: true}, + updated_by: {type: 'string', maxlength: 24, nullable: true} } }; diff --git a/core/server/models/api-key.js b/core/server/models/api-key.js new file mode 100644 index 0000000000..cf34d9b439 --- /dev/null +++ b/core/server/models/api-key.js @@ -0,0 +1,56 @@ +const crypto = require('crypto'); +const ghostBookshelf = require('./base'); +const {Role} = require('./role'); + +const ApiKey = ghostBookshelf.Model.extend({ + tableName: 'api_keys', + + defaults() { + // 512bit key for HS256 JWT signing + const secret = crypto.randomBytes(64).toString('hex'); + + return { + secret + }; + }, + + role() { + return this.belongsTo('Role'); + }, + + // if an ApiKey does not have a related Integration then it's considered + // "internal" and shouldn't show up in the UI. Example internal API Keys + // would be the ones used for the scheduler and backup clients + integration() { + return this.belongsTo('Integration'); + }, + + onSaving(/* model, attrs, options */) { + ghostBookshelf.Model.prototype.onSaving.apply(this, arguments); + + // enforce roles which are currently hardcoded + // - admin key = Adminstrator role + // - content key = no role + if (this.hasChanged('type') || this.hasChanged('role_id')) { + if (this.get('type') === 'admin') { + return Role.findOne({name: 'Admin Integration'}, {columns: ['id']}) + .then((role) => { + this.set('role_id', role.get('id')); + }); + } + + if (this.get('type') === 'content') { + this.set('role_id', null); + } + } + } +}); + +const ApiKeys = ghostBookshelf.Collection.extend({ + model: ApiKey +}); + +module.exports = { + ApiKey: ghostBookshelf.model('ApiKey', ApiKey), + ApiKeys: ghostBookshelf.collection('ApiKeys', ApiKeys) +}; diff --git a/core/server/models/index.js b/core/server/models/index.js index 3131f1e083..d9d71fa5b5 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -31,7 +31,9 @@ models = [ 'tag', 'user', 'invite', - 'webhook' + 'webhook', + 'integration', + 'api-key' ]; function init() { diff --git a/core/server/models/integration.js b/core/server/models/integration.js new file mode 100644 index 0000000000..731186f813 --- /dev/null +++ b/core/server/models/integration.js @@ -0,0 +1,18 @@ +const ghostBookshelf = require('./base'); + +const Integration = ghostBookshelf.Model.extend({ + tableName: 'integrations', + + api_keys: function apiKeys() { + return this.hasMany('ApiKey'); + } +}); + +const Integrations = ghostBookshelf.Collection.extend({ + model: Integration +}); + +module.exports = { + Integration: ghostBookshelf.model('Integration', Integration), + Integrations: ghostBookshelf.collection('Integrations', Integrations) +}; diff --git a/core/server/models/role.js b/core/server/models/role.js index c8850f35db..9bf7d857f2 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -10,12 +10,22 @@ Role = ghostBookshelf.Model.extend({ tableName: 'roles', + relationships: ['permissions'], + + relationshipBelongsTo: { + permissions: 'permissions' + }, + users: function users() { return this.belongsToMany('User'); }, permissions: function permissions() { return this.belongsToMany('Permission'); + }, + + api_keys: function apiKeys() { + return this.hasMany('ApiKey'); } }, { /** diff --git a/core/test/functional/routes/api/db_spec.js b/core/test/functional/routes/api/db_spec.js index c1d6523da6..98c176b6d2 100644 --- a/core/test/functional/routes/api/db_spec.js +++ b/core/test/functional/routes/api/db_spec.js @@ -72,7 +72,7 @@ describe('DB API', function () { var jsonResponse = res.body; should.exist(jsonResponse.db); jsonResponse.db.should.have.length(1); - Object.keys(jsonResponse.db[0].data).length.should.eql(22); + Object.keys(jsonResponse.db[0].data).length.should.eql(24); done(); }); }); @@ -90,7 +90,7 @@ describe('DB API', function () { const jsonResponse = res.body; should.exist(jsonResponse.db); jsonResponse.db.should.have.length(1); - Object.keys(jsonResponse.db[0].data).length.should.eql(24); + Object.keys(jsonResponse.db[0].data).length.should.eql(26); done(); }); }); diff --git a/core/test/integration/api/api_roles_spec.js b/core/test/integration/api/api_roles_spec.js index b616c82ff8..a39d26a7d7 100644 --- a/core/test/integration/api/api_roles_spec.js +++ b/core/test/integration/api/api_roles_spec.js @@ -16,12 +16,13 @@ describe('Roles API', function () { should.exist(response); testUtils.API.checkResponse(response, 'roles'); should.exist(response.roles); - response.roles.should.have.length(5); + response.roles.should.have.length(6); testUtils.API.checkResponse(response.roles[0], 'role'); testUtils.API.checkResponse(response.roles[1], 'role'); testUtils.API.checkResponse(response.roles[2], 'role'); testUtils.API.checkResponse(response.roles[3], 'role'); testUtils.API.checkResponse(response.roles[4], 'role'); + testUtils.API.checkResponse(response.roles[5], 'role'); } it('Owner can browse', function (done) { diff --git a/core/test/integration/migration_spec.js b/core/test/integration/migration_spec.js index c3e9dad514..928d2b56dc 100644 --- a/core/test/integration/migration_spec.js +++ b/core/test/integration/migration_spec.js @@ -44,125 +44,155 @@ describe('Database Migration (special functions)', function () { // Mail permissions[3].name.should.eql('Send mail'); - permissions[3].should.be.AssignedToRoles(['Administrator']); + permissions[3].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); // Notifications permissions[4].name.should.eql('Browse notifications'); - permissions[4].should.be.AssignedToRoles(['Administrator']); + permissions[4].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[5].name.should.eql('Add notifications'); - permissions[5].should.be.AssignedToRoles(['Administrator']); + permissions[5].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[6].name.should.eql('Delete notifications'); - permissions[6].should.be.AssignedToRoles(['Administrator']); + permissions[6].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); // Posts permissions[7].name.should.eql('Browse posts'); - permissions[7].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[7].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[8].name.should.eql('Read posts'); - permissions[8].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[8].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[9].name.should.eql('Edit posts'); - permissions[9].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[9].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[10].name.should.eql('Add posts'); - permissions[10].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[10].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[11].name.should.eql('Delete posts'); - permissions[11].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[11].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); // Settings permissions[12].name.should.eql('Browse settings'); - permissions[12].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[12].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[13].name.should.eql('Read settings'); - permissions[13].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[13].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[14].name.should.eql('Edit settings'); - permissions[14].should.be.AssignedToRoles(['Administrator']); + permissions[14].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); // Slugs permissions[15].name.should.eql('Generate slugs'); - permissions[15].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[15].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); // Tags permissions[16].name.should.eql('Browse tags'); - permissions[16].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[16].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[17].name.should.eql('Read tags'); - permissions[17].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[17].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[18].name.should.eql('Edit tags'); - permissions[18].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[18].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[19].name.should.eql('Add tags'); - permissions[19].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']); + permissions[19].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Admin Integration']); permissions[20].name.should.eql('Delete tags'); - permissions[20].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[20].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); // Themes permissions[21].name.should.eql('Browse themes'); - permissions[21].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[21].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[22].name.should.eql('Edit themes'); - permissions[22].should.be.AssignedToRoles(['Administrator']); + permissions[22].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[23].name.should.eql('Activate themes'); - permissions[23].should.be.AssignedToRoles(['Administrator']); + permissions[23].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[24].name.should.eql('Upload themes'); - permissions[24].should.be.AssignedToRoles(['Administrator']); + permissions[24].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[25].name.should.eql('Download themes'); - permissions[25].should.be.AssignedToRoles(['Administrator']); + permissions[25].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[26].name.should.eql('Delete themes'); - permissions[26].should.be.AssignedToRoles(['Administrator']); + permissions[26].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); // Users permissions[27].name.should.eql('Browse users'); - permissions[27].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[27].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[28].name.should.eql('Read users'); - permissions[28].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[28].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[29].name.should.eql('Edit users'); - permissions[29].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[29].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[30].name.should.eql('Add users'); - permissions[30].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[30].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[31].name.should.eql('Delete users'); - permissions[31].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[31].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); // Roles permissions[32].name.should.eql('Assign a role'); - permissions[32].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[32].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[33].name.should.eql('Browse roles'); - permissions[33].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[33].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); // Clients permissions[34].name.should.eql('Browse clients'); - permissions[34].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[34].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[35].name.should.eql('Read clients'); - permissions[35].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[35].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[36].name.should.eql('Edit clients'); - permissions[36].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[36].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[37].name.should.eql('Add clients'); - permissions[37].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[37].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[38].name.should.eql('Delete clients'); - permissions[38].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[38].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); // Subscribers permissions[39].name.should.eql('Browse subscribers'); - permissions[39].should.be.AssignedToRoles(['Administrator']); + permissions[39].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[40].name.should.eql('Read subscribers'); - permissions[40].should.be.AssignedToRoles(['Administrator']); + permissions[40].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[41].name.should.eql('Edit subscribers'); - permissions[41].should.be.AssignedToRoles(['Administrator']); + permissions[41].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[42].name.should.eql('Add subscribers'); - permissions[42].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor']); + permissions[42].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration']); permissions[43].name.should.eql('Delete subscribers'); - permissions[43].should.be.AssignedToRoles(['Administrator']); + permissions[43].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); // Invites permissions[44].name.should.eql('Browse invites'); - permissions[44].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[44].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[45].name.should.eql('Read invites'); - permissions[45].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[45].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[46].name.should.eql('Edit invites'); - permissions[46].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[46].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[47].name.should.eql('Add invites'); - permissions[47].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[47].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); permissions[48].name.should.eql('Delete invites'); - permissions[48].should.be.AssignedToRoles(['Administrator', 'Editor']); + permissions[48].should.be.AssignedToRoles(['Administrator', 'Editor', 'Admin Integration']); // Redirects permissions[49].name.should.eql('Download redirects'); - permissions[49].should.be.AssignedToRoles(['Administrator']); + permissions[49].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); permissions[50].name.should.eql('Upload redirects'); - permissions[50].should.be.AssignedToRoles(['Administrator']); + permissions[50].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); + + // Webhooks + permissions[51].name.should.eql('Add webhooks'); + permissions[51].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); + permissions[52].name.should.eql('Delete webhooks'); + permissions[52].should.be.AssignedToRoles(['Administrator', 'Admin Integration']); + + // Integrations + permissions[53].name.should.eql('Browse integrations'); + permissions[53].should.be.AssignedToRoles(['Administrator']); + permissions[54].name.should.eql('Read integrations'); + permissions[54].should.be.AssignedToRoles(['Administrator']); + permissions[55].name.should.eql('Edit integrations'); + permissions[55].should.be.AssignedToRoles(['Administrator']); + permissions[56].name.should.eql('Add integrations'); + permissions[56].should.be.AssignedToRoles(['Administrator']); + permissions[57].name.should.eql('Delete integrations'); + permissions[57].should.be.AssignedToRoles(['Administrator']); + + // API Keys + permissions[58].name.should.eql('Browse API keys'); + permissions[58].should.be.AssignedToRoles(['Administrator']); + permissions[59].name.should.eql('Read API keys'); + permissions[59].should.be.AssignedToRoles(['Administrator']); + permissions[60].name.should.eql('Edit API keys'); + permissions[60].should.be.AssignedToRoles(['Administrator']); + permissions[61].name.should.eql('Add API keys'); + permissions[61].should.be.AssignedToRoles(['Administrator']); + permissions[62].name.should.eql('Delete API keys'); + permissions[62].should.be.AssignedToRoles(['Administrator']); }); describe('Populate', function () { @@ -217,15 +247,16 @@ describe('Database Migration (special functions)', function () { // Roles should.exist(result.roles); - result.roles.length.should.eql(5); + result.roles.length.should.eql(6); result.roles.at(0).get('name').should.eql('Administrator'); result.roles.at(1).get('name').should.eql('Editor'); result.roles.at(2).get('name').should.eql('Author'); result.roles.at(3).get('name').should.eql('Contributor'); result.roles.at(4).get('name').should.eql('Owner'); + result.roles.at(5).get('name').should.eql('Admin Integration'); // Permissions - result.permissions.length.should.eql(53); + result.permissions.length.should.eql(63); result.permissions.toJSON().should.be.CompletePermissions(); }); }); diff --git a/core/test/integration/model/model_roles_spec.js b/core/test/integration/model/model_roles_spec.js index 0ce1332179..f09ce1cf0a 100644 --- a/core/test/integration/model/model_roles_spec.js +++ b/core/test/integration/model/model_roles_spec.js @@ -76,7 +76,7 @@ describe('Role Model', function () { return RoleModel.destroy(firstRole); }).then(function (response) { - response.toJSON().should.be.empty(); + response.toJSON().permissions.should.be.empty(); return RoleModel.findOne(firstRole); }).then(function (newResults) { should.equal(newResults, null); diff --git a/core/test/unit/data/schema/fixtures/utils_spec.js b/core/test/unit/data/schema/fixtures/utils_spec.js index 763c822939..a4ce7cfef6 100644 --- a/core/test/unit/data/schema/fixtures/utils_spec.js +++ b/core/test/unit/data/schema/fixtures/utils_spec.js @@ -152,19 +152,19 @@ describe('Migration Fixture Utils', function () { fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) { should.exist(result); result.should.be.an.Object(); - result.should.have.property('expected', 43); - result.should.have.property('done', 43); + result.should.have.property('expected', 59); + result.should.have.property('done', 59); // Permissions & Roles permsAllStub.calledOnce.should.be.true(); rolesAllStub.calledOnce.should.be.true(); - dataMethodStub.filter.callCount.should.eql(43); - dataMethodStub.find.callCount.should.eql(4); - baseUtilAttachStub.callCount.should.eql(43); + dataMethodStub.filter.callCount.should.eql(59); + dataMethodStub.find.callCount.should.eql(5); + baseUtilAttachStub.callCount.should.eql(59); - fromItem.related.callCount.should.eql(43); - fromItem.findWhere.callCount.should.eql(43); - toItem[0].get.callCount.should.eql(86); + fromItem.related.callCount.should.eql(59); + fromItem.findWhere.callCount.should.eql(59); + toItem[0].get.callCount.should.eql(118); done(); }).catch(done); diff --git a/core/test/unit/data/schema/integrity_spec.js b/core/test/unit/data/schema/integrity_spec.js index a43a2c4d82..10ef28950c 100644 --- a/core/test/unit/data/schema/integrity_spec.js +++ b/core/test/unit/data/schema/integrity_spec.js @@ -19,8 +19,8 @@ var should = require('should'), */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = 'be8d6ff382ae07b238c60d5b453a2944'; - const currentFixturesHash = 'eab42b1e9cd754e76600f1c57c4a7af8'; + const currentSchemaHash = '1834b95684f1916f79e51bab8d6eac8f'; + const currentFixturesHash = '20292edf9fd692cbd6485267a2ac8e75'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, // and the values above will need updating as confirmation diff --git a/core/test/unit/models/api-key_spec.js b/core/test/unit/models/api-key_spec.js new file mode 100644 index 0000000000..7608a0c3d9 --- /dev/null +++ b/core/test/unit/models/api-key_spec.js @@ -0,0 +1,66 @@ +const models = require('../../../server/models'); +const should = require('should'); +const testUtils = require('../../utils'); + +describe('Unit: models/api_key', function () { + before(models.init); + before(testUtils.teardown); + before(testUtils.setup('roles')); + + describe('Add', function () { + it('sets default secret', function () { + // roles[5] = 'Admin Integration' + const role_id = testUtils.DataGenerator.forKnex.roles[5].id; + const attrs = { + type: 'admin', + role_id + }; + + return models.ApiKey.add(attrs).then((api_key) => { + return models.ApiKey.findOne({id: api_key.id}, {withRelated: ['role']}) + .then((api_key) => { + api_key.get('type').should.eql('admin'); + api_key.related('role').get('id').should.eql(role_id); + + // defaults + api_key.get('secret').length.should.eql(128); + }); + }); + }); + + it('sets hardcoded role for key type', function () { + // roles[5] = 'Admin Integration' + const role_id = testUtils.DataGenerator.forKnex.roles[5].id; + + const adminKey = { + type: 'admin' + }; + const adminCheck = models.ApiKey.add(adminKey).then((api_key) => { + return models.ApiKey.findOne({id: api_key.id}, {withRelated: ['role']}) + .then((api_key) => { + api_key.get('type').should.eql('admin'); + + // defaults + should.exist(api_key.related('role').id); + api_key.related('role').get('id').should.eql(role_id); + }); + }); + + const contentKey = { + type: 'content', + role_id: testUtils.DataGenerator.forKnex.roles[0].id + }; + const contentCheck = models.ApiKey.add(contentKey).then((api_key) => { + return models.ApiKey.findOne({id: api_key.id}, {withRelated: ['role']}) + .then((api_key) => { + api_key.get('type').should.eql('content'); + + // defaults + should.not.exist(api_key.related('role').id); + }); + }); + + return Promise.all([adminCheck, contentCheck]); + }); + }); +}); diff --git a/core/test/unit/models/role_spec.js b/core/test/unit/models/role_spec.js new file mode 100644 index 0000000000..12c3ab3b2e --- /dev/null +++ b/core/test/unit/models/role_spec.js @@ -0,0 +1,28 @@ +const models = require('../../../server/models'); +const ghostBookshelf = require('../../../server/models/base'); +const testUtils = require('../../utils'); +const should = require('should'); + +describe('Unit: models/role', function () { + before(testUtils.teardown); + before(testUtils.setup('roles', 'perms:role')); + + describe('destroy', function () { + it('cleans up permissions join table', function () { + const adminRole = {id: testUtils.DataGenerator.Content.roles[0].id}; + + function checkRolePermissionsCount(count) { + return ghostBookshelf.knex.select().table('permissions_roles').where('role_id', adminRole.id) + .then((rolePermissions) => { + rolePermissions.length.should.eql(count); + }); + } + + return models.Role.findOne(adminRole) + .then(role => should.exist(role, 'Administrator role not found')) + .then(() => checkRolePermissionsCount(2)) + .then(() => models.Role.destroy(adminRole)) + .then(() => checkRolePermissionsCount(0)); + }); + }); +}); diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index 8cea1d30b2..30000bca46 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -288,6 +288,11 @@ DataGenerator.Content = { id: ObjectId.generate(), name: 'Contributor', description: 'Contributors' + }, + { + id: ObjectId.generate(), + name: 'Admin Integration', + description: 'External Apps' } ], @@ -367,25 +372,41 @@ DataGenerator.Content = { event: 'subscriber.removed', target_url: 'https://example.com/webhooks/subscriber-removed' } + ], + + integrations: [ + { + id: ObjectId.generate(), + name: 'Test Integration', + slug: 'test-integration' + } + ], + + api_keys: [ + { + id: ObjectId.generate(), + type: 'admin' + // integration_id: DataGenerator.Content.integrations[0].id + }, + { + id: ObjectId.generate(), + type: 'content' + // integration_id: DataGenerator.Content.integrations[0].id + }, + { + id: ObjectId.generate(), + type: 'admin', + integration_id: undefined // "internal" + } ] }; +// set up belongs_to relationships DataGenerator.Content.subscribers[0].post_id = DataGenerator.Content.posts[0].id; +DataGenerator.Content.api_keys[0].integration_id = DataGenerator.Content.integrations[0].id; +DataGenerator.Content.api_keys[1].integration_id = DataGenerator.Content.integrations[0].id; DataGenerator.forKnex = (function () { - var posts, - tags, - posts_tags, - posts_authors, - apps, - app_fields, - roles, - users, - roles_users, - clients, - invites, - webhooks; - function createBasic(overrides) { var newObj = _.cloneDeep(overrides); @@ -648,7 +669,7 @@ DataGenerator.forKnex = (function () { }); } - posts = [ + const posts = [ createPost(DataGenerator.Content.posts[0]), createPost(DataGenerator.Content.posts[1]), createPost(DataGenerator.Content.posts[2]), @@ -659,7 +680,7 @@ DataGenerator.forKnex = (function () { createPost(DataGenerator.Content.posts[7]) ]; - tags = [ + const tags = [ createTag(DataGenerator.Content.tags[0]), createTag(DataGenerator.Content.tags[1]), createTag(DataGenerator.Content.tags[2]), @@ -667,15 +688,16 @@ DataGenerator.forKnex = (function () { createTag(DataGenerator.Content.tags[4]) ]; - roles = [ + const roles = [ createBasic(DataGenerator.Content.roles[0]), createBasic(DataGenerator.Content.roles[1]), createBasic(DataGenerator.Content.roles[2]), createBasic(DataGenerator.Content.roles[3]), - createBasic(DataGenerator.Content.roles[4]) + createBasic(DataGenerator.Content.roles[4]), + createBasic(DataGenerator.Content.roles[5]) ]; - users = [ + const users = [ createUser(DataGenerator.Content.users[0]), createUser(DataGenerator.Content.users[1]), createUser(DataGenerator.Content.users[2]), @@ -683,14 +705,14 @@ DataGenerator.forKnex = (function () { createUser(DataGenerator.Content.users[7]) ]; - clients = [ + const clients = [ createClient({name: 'Ghost Admin', slug: 'ghost-admin', type: 'ua'}), createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'}), createClient({name: 'Ghost Auth', slug: 'ghost-auth', type: 'web'}), createClient({name: 'Ghost Backup', slug: 'ghost-backup', type: 'web'}) ]; - roles_users = [ + const roles_users = [ { id: ObjectId.generate(), user_id: DataGenerator.Content.users[0].id, @@ -720,7 +742,7 @@ DataGenerator.forKnex = (function () { // this is not pretty, but the fastest // it relies on the created posts/tags - posts_tags = [ + const posts_tags = [ { id: ObjectId.generate(), post_id: DataGenerator.Content.posts[0].id, @@ -759,7 +781,7 @@ DataGenerator.forKnex = (function () { } ]; - posts_authors = [ + const posts_authors = [ { id: ObjectId.generate(), post_id: DataGenerator.Content.posts[0].id, @@ -816,27 +838,37 @@ DataGenerator.forKnex = (function () { } ]; - apps = [ + const apps = [ createBasic(DataGenerator.Content.apps[0]), createBasic(DataGenerator.Content.apps[1]), createBasic(DataGenerator.Content.apps[2]) ]; - app_fields = [ + const app_fields = [ createAppField(DataGenerator.Content.app_fields[0]), createAppField(DataGenerator.Content.app_fields[1]) ]; - invites = [ + 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}) ]; - webhooks = [ + const webhooks = [ createWebhook(DataGenerator.Content.webhooks[0]), createWebhook(DataGenerator.Content.webhooks[1]) ]; + const integrations = [ + createBasic(DataGenerator.Content.integrations[0]) + ]; + + const api_keys = [ + createBasic(DataGenerator.Content.api_keys[0]), + createBasic(DataGenerator.Content.api_keys[1]), + createBasic(DataGenerator.Content.api_keys[2]), + ]; + return { createPost: createPost, createGenericPost: createGenericPost, @@ -871,7 +903,9 @@ DataGenerator.forKnex = (function () { users: users, roles_users: roles_users, clients: clients, - webhooks: webhooks + webhooks: webhooks, + integrations: integrations, + api_keys: api_keys }; }()); diff --git a/core/test/utils/index.js b/core/test/utils/index.js index 26ad1651de..729087e15f 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -401,7 +401,8 @@ fixtures = { Editor: DataGenerator.Content.roles[1].id, Author: DataGenerator.Content.roles[2].id, Owner: DataGenerator.Content.roles[3].id, - Contributor: DataGenerator.Content.roles[4].id + Contributor: DataGenerator.Content.roles[4].id, + 'Admin Integration': DataGenerator.Content.roles[5].id }; // CASE: if empty db will throw SQLITE_MISUSE, hard to debug @@ -478,7 +479,19 @@ fixtures = { return Promise.map(DataGenerator.forKnex.webhooks, function (webhook) { return models.Webhook.add(webhook, module.exports.context.internal); }); - } + }, + + insertIntegrations: function insertIntegrations() { + return Promise.map(DataGenerator.forKnex.integrations, function (integration) { + return models.Integration.add(integration, module.exports.context.internal); + }); + }, + + insertApiKeys: function insertApiKeys() { + return Promise.map(DataGenerator.forKnex.api_keys, function (api_key) { + return models.ApiKey.add(api_key, module.exports.context.internal); + }); + }, }; /** Test Utility Functions **/ @@ -624,6 +637,12 @@ toDoList = { }, webhooks: function insertWebhooks() { return fixtures.insertWebhooks(); + }, + integrations: function insertIntegrations() { + return fixtures.insertIntegrations(); + }, + api_keys: function insertApiKeys() { + return fixtures.insertApiKeys(); } }; @@ -1139,7 +1158,9 @@ module.exports = { admin: {context: {user: DataGenerator.Content.users[1].id}}, editor: {context: {user: DataGenerator.Content.users[2].id}}, author: {context: {user: DataGenerator.Content.users[3].id}}, - contributor: {context: {user: DataGenerator.Content.users[7].id}} + contributor: {context: {user: DataGenerator.Content.users[7].id}}, + admin_api_key: {context: {api_key: DataGenerator.Content.api_keys[0].id}}, + content_api_key: {context: {api_key: DataGenerator.Content.api_keys[1].id}} }, permissions: { owner: {user: {roles: [DataGenerator.Content.roles[3]]}},