Ghost/core/server/models/api-key.js
Thibaut Patel 214ed405cc Added the user_id field to the api_keys table
no issue

- Migration related to the Personal Tokens feature.
2020-11-20 09:53:18 +01:00

160 lines
4.0 KiB
JavaScript

const omit = require('lodash/omit');
const logging = require('../../shared/logging');
const errors = require('@tryghost/errors');
const _ = require('lodash');
const crypto = require('crypto');
const ghostBookshelf = require('./base');
const {Role} = require('./role');
/*
* Uses birthday problem estimation to calculate chance of collision
* d = 16^26 // 26 char hex string
* n = 10,000,000 // 10 million
*
* (-n x (n-1)) / 2d
* 1 - e^
*
*
* 17
* ~= 4 x 10^
*
* ref: https://medium.freecodecamp.org/how-long-should-i-make-my-api-key-833ebf2dc26f
* ref: https://en.wikipedia.org/wiki/Birthday_problem#Approximations
*
* 26 char hex string = 13 bytes
* 64 char hex string JWT secret = 32 bytes
*/
const createSecret = (type) => {
const bytes = type === 'content' ? 13 : 32;
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 existingAction = model.getAction(event, options);
// CASE: model does not support action for target event
if (!existingAction) {
return;
}
const insert = (action) => {
ghostBookshelf.model('Action')
.add(action)
.catch((err) => {
if (_.isArray(err)) {
err = err[0];
}
logging.error(new errors.InternalServerError({
err
}));
});
};
if (options.transacting) {
options.transacting.once('committed', (committed) => {
if (!committed) {
return;
}
insert(existingAction);
});
} else {
insert(existingAction);
}
};
const ApiKey = ghostBookshelf.Model.extend({
tableName: 'api_keys',
defaults() {
const secret = createSecret(this.get('type'));
return {
secret
};
},
role() {
return this.belongsTo('Role');
},
integration() {
return this.belongsTo('Integration');
},
user() {
return this.belongsTo('User');
},
format(attrs) {
return omit(attrs, 'role');
},
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: attrs.role || 'Admin Integration'}, Object.assign({}, options, {columns: ['id']}))
.then((role) => {
this.set('role_id', role.get('id'));
});
}
if (this.get('type') === 'content') {
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) {
const secret = createSecret(data.type);
return this.edit(Object.assign({}, data, {secret}), options);
}
});
const ApiKeys = ghostBookshelf.Collection.extend({
model: ApiKey
});
module.exports = {
ApiKey: ghostBookshelf.model('ApiKey', ApiKey),
ApiKeys: ghostBookshelf.collection('ApiKeys', ApiKeys)
};