diff --git a/CHANGES b/CHANGES index 0f5e7e3c6..0cc2ae934 100644 --- a/CHANGES +++ b/CHANGES @@ -2959,4 +2959,5 @@ * profile: avatar cannot be changed when profile is locked * app backup: no more part alters app state. runs completely in background * system: disk usage is not collected in background. new disk ui, computes space on demand +* backups: multiple backup targets diff --git a/src/backuptargets.js b/src/backuptargets.js index 1f3dee084..ec39fe26a 100644 --- a/src/backuptargets.js +++ b/src/backuptargets.js @@ -189,52 +189,63 @@ async function update(target, data) { if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found'); } -async function setSchedule(target, schedule) { - assert.strictEqual(typeof target, 'object'); +async function setSchedule(backupTarget, schedule, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof schedule, 'string'); + assert.strictEqual(typeof auditSource, 'object'); const error = await validateSchedule(schedule); if (error) throw error; - await update(target, { schedule }); + await update(backupTarget, { schedule }); - await cron.handleBackupScheduleChanged(target); + await cron.handleBackupScheduleChanged(backupTarget); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, schedule }); } -async function setLimits(target, limits) { - assert.strictEqual(typeof target, 'object'); +async function setLimits(backupTarget, limits, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof limits, 'object'); + assert.strictEqual(typeof auditSource, 'object'); - await update(target, { limits }); + await update(backupTarget, { limits }); + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, limits }); } -async function setRetention(target, retention) { - assert.strictEqual(typeof target, 'object'); +async function setRetention(backupTarget, retention, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof retention, 'object'); + assert.strictEqual(typeof auditSource, 'object'); const error = await validateRetention(retention); if (error) throw error; - await update(target, { retention }); + await update(backupTarget, { retention }); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, retention }); } -async function setPrimary(target) { - assert.strictEqual(typeof target, 'object'); +async function setPrimary(backupTarget, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); + assert.strictEqual(typeof auditSource, 'object'); const queries = [ - { query: 'SELECT 1 FROM backupTargets WHERE id=? FOR UPDATE', args: [ target.id ] }, // ensure this exists! + { query: 'SELECT 1 FROM backupTargets WHERE id=? FOR UPDATE', args: [ backupTarget.id ] }, // ensure this exists! { query: 'UPDATE backupTargets SET main=?', args: [ false ] }, - { query: 'UPDATE backupTargets SET main=? WHERE id=?', args: [ true, target.id ] } + { query: 'UPDATE backupTargets SET main=? WHERE id=?', args: [ true, backupTarget.id ] } ]; const [error, result] = await safe(database.transaction(queries)); if (error) throw error; if (result[2].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found'); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, primary: true }); } async function del(target, auditSource) { assert.strictEqual(typeof target, 'object'); - assert(auditSource && typeof auditSource === 'object'); + assert.strictEqual(typeof auditSource, 'object'); if (target.primary) throw new BoxError(BoxError.CONFLICT, 'Cannot delete the primary backup target'); @@ -247,7 +258,7 @@ async function del(target, auditSource) { if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error); if (error) throw error; if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found'); - // await eventlog.add(eventlog.ACTION_ARCHIVES_DEL, auditSource, { id: archive.id, backupId: archive.backupId }); + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_REMOVE, auditSource, { backupTarget: target }); target.schedule = constants.CRON_PATTERN_NEVER; await cron.handleBackupScheduleChanged(target); @@ -385,33 +396,37 @@ async function ensureMounted(target) { return await getMountStatus(target); } -async function setConfig(target, newConfig) { - assert.strictEqual(typeof target, 'object'); +async function setConfig(backupTarget, newConfig, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof newConfig, 'object'); + assert.strictEqual(typeof auditSource, 'object'); if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode'); - const oldConfig = target.config; + const oldConfig = backupTarget.config; - storage.api(target.provider).injectPrivateFields(newConfig, oldConfig); + storage.api(backupTarget.provider).injectPrivateFields(newConfig, oldConfig); debug('setConfig: validating new storage configuration'); - await storage.testMount(target.provider, newConfig, '/mnt/backup-storage-validation'); + await storage.testMount(backupTarget.provider, newConfig, '/mnt/backup-storage-validation'); debug('setConfig: removing old storage configuration'); - if (mounts.isManagedProvider(target.provider)) await safe(mounts.removeMount(managedBackupMountObject(oldConfig))); + if (mounts.isManagedProvider(backupTarget.provider)) await safe(mounts.removeMount(managedBackupMountObject(oldConfig))); debug('setConfig: setting up new storage configuration'); - await storage.setupManagedMount(target.provider, newConfig, paths.MANAGED_BACKUP_MOUNT_DIR); + await storage.setupManagedMount(backupTarget.provider, newConfig, paths.MANAGED_BACKUP_MOUNT_DIR); debug('setConfig: clearing backup cache'); - cleanupCacheFilesSync(target); + cleanupCacheFilesSync(backupTarget); - await update(target, { config: newConfig }); + await update(backupTarget, { config: newConfig }); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, newConfig }); } -async function add(data) { +async function add(data, auditSource) { assert.strictEqual(typeof data, 'object'); + assert.strictEqual(typeof auditSource, 'object'); if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode'); @@ -443,5 +458,8 @@ async function add(data) { const id = `bc-${uuid.v4()}`; await database.query('INSERT INTO backupTargets (id, label, provider, configJson, limitsJson, retentionJson, schedule, encryptionJson, format, main) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ id, label, provider, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, false ]); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_ADD, auditSource, { id, label, provider, config, schedule, format }); + return id; } diff --git a/src/eventlog.js b/src/eventlog.js index 668579e96..559f9493f 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -38,6 +38,10 @@ exports = module.exports = { ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start', // obsolete ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish', + ACTION_BACKUP_TARGET_ADD: 'backuptarget.add', + ACTION_BACKUP_TARGET_REMOVE: 'backuptarget.remove', + ACTION_BACKUP_TARGET_UPDATE: 'backuptarget.update', + ACTION_BRANDING_NAME: 'branding.name', ACTION_BRANDING_FOOTER: 'branding.footer', ACTION_BRANDING_AVATAR: 'branding.avatar', diff --git a/src/routes/backuptargets.js b/src/routes/backuptargets.js index 5d0bf6bbe..072f85343 100644 --- a/src/routes/backuptargets.js +++ b/src/routes/backuptargets.js @@ -93,7 +93,7 @@ async function add(req, res, next) { // testing the backup using put/del takes a bit of time at times req.clearTimeout(); - const [error, id] = await safe(backupTargets.add(req.body)); + const [error, id] = await safe(backupTargets.add(req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { id })); @@ -139,7 +139,7 @@ async function setLimits(req, res, next) { if ('memoryLimit' in limits && typeof limits.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit must be a positive integer')); - const [error] = await safe(backupTargets.setLimits(req.resources.backupTarget, limits)); + const [error] = await safe(backupTargets.setLimits(req.resources.backupTarget, limits, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -153,7 +153,7 @@ async function setConfig(req, res, next) { // testing the backup using put/del takes a bit of time at times req.clearTimeout(); - const [error] = await safe(backupTargets.setConfig(req.resources.backupTarget, req.body.config)); + const [error] = await safe(backupTargets.setConfig(req.resources.backupTarget, req.body.config, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -164,7 +164,7 @@ async function setSchedule(req, res, next) { if (typeof req.body.schedule !== 'string') return next(new HttpError(400, 'schedule is required')); - const [error] = await safe(backupTargets.setSchedule(req.resources.backupTarget, req.body.schedule)); + const [error] = await safe(backupTargets.setSchedule(req.resources.backupTarget, req.body.schedule, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -175,7 +175,7 @@ async function setRetention(req, res, next) { if (!req.body.retention || typeof req.body.retention !== 'object') return next(new HttpError(400, 'retention is required')); - const [error] = await safe(backupTargets.setRetention(req.resources.backupTarget, req.body.retention)); + const [error] = await safe(backupTargets.setRetention(req.resources.backupTarget, req.body.retention, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -184,7 +184,7 @@ async function setRetention(req, res, next) { async function setPrimary(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - const [error] = await safe(backupTargets.setPrimary(req.resources.backupTarget)); + const [error] = await safe(backupTargets.setPrimary(req.resources.backupTarget, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); diff --git a/src/routes/test/backuptargets-test.js b/src/routes/test/backuptargets-test.js index 2f7538e80..b3beab1f7 100644 --- a/src/routes/test/backuptargets-test.js +++ b/src/routes/test/backuptargets-test.js @@ -100,7 +100,6 @@ describe('Backups API', function () { expect(response.body.format).to.be(encryptedTarget.format); expect(response.body.label).to.be(encryptedTarget.label); expect(response.body.primary).to.be(false); - console.log(response.body); expect(response.body.password).to.be.ok(); expect(response.body.encryptedFilenames).to.be(true); }); diff --git a/src/routes/test/common.js b/src/routes/test/common.js index 6e0cba172..df90014c1 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -125,8 +125,8 @@ async function setupServer() { format: 'tgz', retention: { keepWithinSecs: 2 * 24 * 60 * 60 }, schedule: '00 00 23 * * *' - }); - await backupTargets.setPrimary({ id }); + }, exports.auditSource); + await backupTargets.setPrimary({ id }, exports.auditSource); await oidcServer.stop(); await server.start(); debug('Set up server complete'); diff --git a/src/test/backupcleaner-test.js b/src/test/backupcleaner-test.js index c53407df1..5aab8b986 100644 --- a/src/test/backupcleaner-test.js +++ b/src/test/backupcleaner-test.js @@ -17,7 +17,7 @@ const archives = require('../archives.js'), timers = require('timers/promises'); describe('backup cleaner', function () { - const { setup, cleanup, app, getDefaultBackupTarget } = common; + const { setup, cleanup, app, getDefaultBackupTarget, auditSource } = common; before(setup); after(cleanup); @@ -238,13 +238,13 @@ describe('backup cleaner', function () { await backupTargets.setConfig(target, { provider: 'filesystem', backupFolder: '/tmp/someplace', - }); - await backupTargets.setRetention(target, { keepWithinSecs: 1 }); - await backupTargets.setSchedule(target, '00 00 23 * * *'); + }, auditSource); + await backupTargets.setRetention(target, { keepWithinSecs: 1 }, auditSource); + await backupTargets.setSchedule(target, '00 00 23 * * *', auditSource); }); async function cleanupBackups(target) { - const taskId = await backupTargets.startCleanupTask(target, { username: 'test' }); + const taskId = await backupTargets.startCleanupTask(target, auditSource); console.log('started task', taskId); diff --git a/src/test/backuptargets-test.js b/src/test/backuptargets-test.js index 8ed521325..1041db9e6 100644 --- a/src/test/backuptargets-test.js +++ b/src/test/backuptargets-test.js @@ -14,7 +14,7 @@ const backupTargets = require('../backuptargets.js'), safe = require('safetydance'); describe('backups', function () { - const { setup, cleanup } = common; + const { setup, cleanup, auditSource } = common; before(async function () { await setup(); @@ -44,33 +44,33 @@ describe('backups', function () { it('can set backup config', async function () { const newConfig = Object.assign({}, defaultBackupTarget.config, { backupFolder: '/tmp/backups' }); - await backupTargets.setConfig(defaultBackupTarget, newConfig); + await backupTargets.setConfig(defaultBackupTarget, newConfig, auditSource); const result = await backupTargets.get(defaultBackupTarget.id); expect(result.config.backupFolder).to.be('/tmp/backups'); }); it('cannot set invalid schedule', async function () { - const [error] = await safe(backupTargets.setSchedule(defaultBackupTarget, '')); + const [error] = await safe(backupTargets.setSchedule(defaultBackupTarget, '', auditSource)); expect(error.reason).to.be(BoxError.BAD_FIELD); }); it('can set valid schedule', async function () { for (const pattern of [ '00 * * * * *', constants.CRON_PATTERN_NEVER ]) { - await backupTargets.setSchedule(defaultBackupTarget, pattern); + await backupTargets.setSchedule(defaultBackupTarget, pattern, auditSource); const backupTarget = await backupTargets.get(defaultBackupTarget.id); expect(backupTarget.schedule).to.be(pattern); } }); it('cannot set invalid retention', async function () { - const [error] = await safe(backupTargets.setRetention(defaultBackupTarget, { keepWhenever: 4 })); + const [error] = await safe(backupTargets.setRetention(defaultBackupTarget, { keepWhenever: 4 }, auditSource)); expect(error.reason).to.be(BoxError.BAD_FIELD); }); it('can set valid retention', async function () { for (const retention of [ { keepWithinSecs: 1 }, { keepYearly: 3 }, { keepMonthly: 14 } ]) { - await backupTargets.setRetention(defaultBackupTarget, retention); + await backupTargets.setRetention(defaultBackupTarget, retention, auditSource); const backupTarget = await backupTargets.get(defaultBackupTarget.id); expect(backupTarget.retention).to.eql(retention); } diff --git a/src/test/backuptask-test.js b/src/test/backuptask-test.js index a916ea357..220e8cbd4 100644 --- a/src/test/backuptask-test.js +++ b/src/test/backuptask-test.js @@ -17,7 +17,7 @@ const backups = require('../backups.js'), timers = require('timers/promises'); describe('backuptask', function () { - const { setup, cleanup, getDefaultBackupTarget } = common; + const { setup, cleanup, getDefaultBackupTarget, auditSource } = common; before(setup); after(cleanup); @@ -35,11 +35,11 @@ describe('backuptask', function () { before(async function () { fs.rmSync(backupConfig.backupFolder, { recursive: true, force: true }); defaultBackupTarget = await getDefaultBackupTarget(); - await backupTargets.setConfig(defaultBackupTarget, backupConfig); + await backupTargets.setConfig(defaultBackupTarget, backupConfig, auditSource); }); async function createBackup(target) { - const taskId = await backupTargets.startBackupTask(target, { username: 'test' }); + const taskId = await backupTargets.startBackupTask(target, auditSource); while (true) { await timers.setTimeout(1000); diff --git a/src/test/common.js b/src/test/common.js index a88b5ea7d..3b8da53b1 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -229,8 +229,8 @@ async function databaseSetup() { format: 'tgz', retention: { keepWithinSecs: 2 * 24 * 60 * 60 }, schedule: '00 00 23 * * *' - }); - await backupTargets.setPrimary({ id }); + }, auditSource); + await backupTargets.setPrimary({ id }, auditSource); } async function domainSetup() { diff --git a/src/test/storage-test.js b/src/test/storage-provider-test.js similarity index 99% rename from src/test/storage-test.js rename to src/test/storage-provider-test.js index 324ce9ad3..d5e1ba58d 100644 --- a/src/test/storage-test.js +++ b/src/test/storage-provider-test.js @@ -24,7 +24,7 @@ const backupTargets = require('../backuptargets.js'), const chunk = s3._chunk; describe('Storage', function () { - const { setup, cleanup, getDefaultBackupTarget } = common; + const { setup, cleanup, getDefaultBackupTarget, auditSource } = common; before(setup); after(cleanup); @@ -52,12 +52,12 @@ describe('Storage', function () { it('fails to set backup storage for bad folder', async function () { const tmp = Object.assign({}, gBackupConfig, { backupFolder: '/root/oof' }); - const [error] = await safe(backupTargets.setConfig(defaultBackupTarget, tmp)); + const [error] = await safe(backupTargets.setConfig(defaultBackupTarget, tmp, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('succeeds to set backup storage', async function () { - await backupTargets.setConfig(defaultBackupTarget, gBackupConfig); + await backupTargets.setConfig(defaultBackupTarget, gBackupConfig, auditSource); expect(fs.existsSync(path.join(gBackupConfig.backupFolder, 'snapshot'))).to.be(true); // auto-created }); diff --git a/src/test/system-test.js b/src/test/system-test.js index 8c1515afd..c23e1e37c 100644 --- a/src/test/system-test.js +++ b/src/test/system-test.js @@ -94,7 +94,7 @@ describe('System', function () { const usageTask = await system.getFilesystemUsage(rootFs.filesystem); return new Promise((resolve, reject) => { - usageTask.on('data', (type, data) => console.log(type, data)); + // usageTask.on('data', (type, data) => console.log(type, data)); usageTask.on('done', (error) => { if (error.errorMessage) reject(new Error(error.errorMessage)); else resolve(); });