Added new endpoint for refreshing api key secret (#11791)

no issue

- Adds new endpoint on integration to refresh admin/content api key secret
- Allows owner/admin to refresh their content or admin API keys for an integration via Ghost Admin
- Adds a new `refreshed` event to actions table for anytime an api_key secret is refreshed
This commit is contained in:
Rishabh Garg 2020-05-05 23:36:21 +05:30 committed by GitHub
parent 21f5912c2d
commit a01bcdd2d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 154 additions and 0 deletions

View File

@ -61,6 +61,7 @@ module.exports = {
],
options: [
'id',
'keyid',
'include'
],
validation: {
@ -74,6 +75,28 @@ module.exports = {
}
},
query({data, options}) {
if (options.keyid) {
return models.ApiKey.findOne({id: options.keyid})
.then(async (model) => {
if (!model) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.resource.resourceNotFound', {
resource: 'ApiKey'
})
});
}
try {
await models.ApiKey.refreshSecret(model.toJSON(), Object.assign({}, options, {id: options.keyid}));
return models.Integration.findOne({id: options.id}, {
withRelated: ['api_keys', 'webhooks']
});
} catch (err) {
throw new common.errors.GhostError({
err: err
});
}
});
}
return models.Integration.edit(data, Object.assign(options, {require: true}))
.catch(models.Integration.NotFoundError, () => {
throw new common.errors.NotFoundError({

View File

@ -1,4 +1,6 @@
const omit = require('lodash/omit');
const common = require('../lib/common');
const _ = require('lodash');
const crypto = require('crypto');
const ghostBookshelf = require('./base');
const {Role} = require('./role');
@ -26,6 +28,50 @@ const createSecret = (type) => {
return crypto.randomBytes(bytes).toString('hex');
};
const addAction = (model, event, options) => {
if (!model.wasChanged()) {
return;
}
// CASE: model does not support actions at all
if (!model.getAction) {
return;
}
const action = model.getAction(event, options);
// CASE: model does not support action for target event
if (!action) {
return;
}
const insert = (action) => {
ghostBookshelf.model('Action')
.add(action)
.catch((err) => {
if (_.isArray(err)) {
err = err[0];
}
common.logging.error(new common.errors.InternalServerError({
err
}));
});
};
if (options.transacting) {
options.transacting.once('committed', (committed) => {
if (!committed) {
return;
}
insert(action);
});
} else {
insert(action);
}
};
const ApiKey = ghostBookshelf.Model.extend({
tableName: 'api_keys',
@ -67,6 +113,29 @@ const ApiKey = ghostBookshelf.Model.extend({
this.set('role_id', null);
}
}
},
onUpdated(model, attrs, options) {
if (this.previous('secret') !== this.get('secret')) {
addAction(model, 'refreshed', options);
}
},
getAction(event, options) {
const actor = this.getActor(options);
// @NOTE: we ignore internal updates (`options.context.internal`) for now
if (!actor) {
return;
}
// @TODO: implement context
return {
event: event,
resource_id: this.id || this.previous('id'),
resource_type: 'api_key',
actor_id: actor.id,
actor_type: actor.type
};
}
}, {
refreshSecret(data, options) {

View File

@ -42,6 +42,7 @@ module.exports = function apiRoutes() {
router.get('/integrations', mw.authAdminApi, http(apiCanary.integrations.browse));
router.get('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.read));
router.post('/integrations', mw.authAdminApi, http(apiCanary.integrations.add));
router.post('/integrations/:id/api_key/:keyid/refresh', mw.authAdminApi, http(apiCanary.integrations.edit));
router.put('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.edit));
router.del('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.destroy));

View File

@ -269,6 +269,67 @@ describe('Integrations API', function () {
});
});
it('Can successfully refresh an integration api key', function (done) {
request.post(localUtils.API.getApiQuery('integrations/?include=api_keys'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Rubbish Integration Name'
}]
})
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [createdIntegration] = body.integrations;
const apiKeys = createdIntegration.api_keys;
const adminApiKey = apiKeys.find(key => key.type === 'admin');
request.post(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/api_key/${adminApiKey.id}/refresh`))
.set('Origin', config.get('url'))
.send({
integrations: [{
id: createdIntegration.id
}]
})
.expect(200)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/?include=api_keys`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [updatedIntegration] = body.integrations;
const updatedAdminApiKey = updatedIntegration.api_keys.find(key => key.type === 'admin');
should.equal(updatedIntegration.id, createdIntegration.id);
updatedAdminApiKey.secret.should.not.eql(adminApiKey.secret);
request.get(localUtils.API.getApiQuery(`actions/?filter=resource_id:${adminApiKey.id}&include=actor`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
const actions = body.actions;
const refreshedAction = actions.find((action) => {
return action.event === 'refreshed';
});
should.exist(refreshedAction);
done();
});
});
});
});
});
it('Can successfully add and delete a created integrations webhooks', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))