Ghost/core/server/api/canary/db.js
Naz aaa54c603c Refactored exporter to use "allowlist" table filtering
refs https://github.com/TryGhost/Team/issues/555

- Previous blocklist approach was resulting in adding every single new table into an export automatically. Which creates possibility to leak sensitive data if not used porperly. Allowlist approach gives better control over what is exported, makes this information explicit, and version-control friendlier
2021-03-25 16:46:56 +13:00

132 lines
4.2 KiB
JavaScript

const Promise = require('bluebird');
const dbBackup = require('../../data/db/backup');
const exporter = require('../../data/exporter');
const importer = require('../../data/importer');
const errors = require('@tryghost/errors');
const models = require('../../models');
module.exports = {
docName: 'db',
backupContent: {
permissions: true,
options: [
'include',
'filename'
],
validation: {
options: {
include: {
values: exporter.BACKUP_TABLES
}
}
},
query(frame) {
// NOTE: we need to have `include` property available as backupDatabase uses it internally
Object.assign(frame.options, {include: frame.options.withRelated});
return dbBackup.backup(frame.options);
}
},
exportContent: {
options: [
'include',
'filename'
],
validation: {
options: {
include: {
values: exporter.BACKUP_TABLES
}
}
},
headers: {
disposition: {
type: 'file',
value: () => (exporter.fileName())
}
},
permissions: true,
async query(frame) {
if (frame.options.filename) {
let backup = await dbBackup.readBackup(frame.options.filename);
if (!backup) {
throw new errors.NotFoundError();
}
return backup;
}
return Promise.resolve()
.then(() => exporter.doExport({include: frame.options.withRelated}))
.catch((err) => {
return Promise.reject(new errors.GhostError({err: err}));
});
}
},
importContent: {
options: [
'include'
],
validation: {
options: {
include: {
values: exporter.BACKUP_TABLES
}
}
},
permissions: true,
query(frame) {
return importer.importFromFile(frame.file, {include: frame.options.withRelated});
}
},
deleteAllContent: {
statusCode: 204,
permissions: true,
query() {
/**
* @NOTE:
* We fetch all posts with `columns:id` to increase the speed of this endpoint.
* And if you trigger `post.destroy(..)`, this will trigger bookshelf and model events.
* But we only have to `id` available in the model. This won't work, because:
* - model layer can't trigger event e.g. `post.page` to trigger `post|page.unpublished`.
* - `onDestroyed` or `onDestroying` can contain custom logic
*/
function deleteContent() {
return models.Base.transaction((transacting) => {
const queryOpts = {
columns: 'id',
context: {internal: true},
destroyAll: true,
transacting: transacting
};
return models.Post.findAll(queryOpts)
.then((response) => {
return Promise.map(response.models, (post) => {
return models.Post.destroy(Object.assign({id: post.id}, queryOpts));
}, {concurrency: 100});
})
.then(() => models.Tag.findAll(queryOpts))
.then((response) => {
return Promise.map(response.models, (tag) => {
return models.Tag.destroy(Object.assign({id: tag.id}, queryOpts));
}, {concurrency: 100});
})
.catch((err) => {
throw new errors.GhostError({
err: err
});
});
});
}
return dbBackup.backup().then(deleteContent);
}
}
};