0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-01 02:41:39 -05:00

Set up schema and models for API Key authentication (#9904)

refs https://github.com/TryGhost/Ghost/issues/9865
- schema migrations
  - adds `integrations` and `api_keys` tables
  - inserts `integration` and `api_key` permissions and Administrator role relationships
  - inserts `Admin Integration` role and permissions
- adds `Integration` model
- adds `ApiKey` model
  - creates default secret if not given
  - hardcodes associated role based on key type
    - `admin` = `Admin API Client`
    - `content` = no role
- updates `Role` model to use `bookshelf-relations` for auto cleanup of permission relationships on destroy
This commit is contained in:
Kevin Ansfield 2018-10-02 17:46:38 +01:00 committed by GitHub
parent ecf47f3b7b
commit 1db3aefb9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 654 additions and 105 deletions

View file

@ -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'});

View file

@ -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);
};

View file

@ -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`));
});
};

View file

@ -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",

View file

@ -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}
}
};

View file

@ -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)
};

View file

@ -31,7 +31,9 @@ models = [
'tag',
'user',
'invite',
'webhook'
'webhook',
'integration',
'api-key'
];
function init() {

View file

@ -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)
};

View file

@ -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');
}
}, {
/**

View file

@ -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();
});
});

View file

@ -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) {

View file

@ -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();
});
});

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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]);
});
});
});

View file

@ -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));
});
});
});

View file

@ -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
};
}());

View file

@ -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]]}},