Skip to content

Commit c4e7ae4

Browse files
committed
Remove unneeded data from database related to Google Drive backups
1 parent f947c12 commit c4e7ae4

File tree

2 files changed

+147
-15
lines changed

2 files changed

+147
-15
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2023 ODK Central Developers
2+
// See the NOTICE file at the top-level directory of this distribution and at
3+
// https://github.com/getodk/central-backend/blob/master/NOTICE.
4+
// This file is part of ODK Central. It is subject to the license terms in
5+
// the LICENSE file found in the top-level directory of this distribution and at
6+
// https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
7+
// including this file, may be copied, modified, propagated, or distributed
8+
// except according to the terms contained in the LICENSE file.
9+
10+
const up = async (db) => {
11+
// Delete configs related to backups.
12+
await db.raw("DELETE FROM config WHERE key IN ('backups.main', 'backups.google')");
13+
14+
// Delete unused backup creation tokens. These three statements mirror those
15+
// in Actors.consume().
16+
const [{ id: roleId }] = await db.select('id').from('roles')
17+
.where({ system: 'initbkup' });
18+
await db.raw(`UPDATE actors SET "deletedAt" = now()
19+
FROM assignments
20+
WHERE
21+
assignments."actorId" = actors.id AND
22+
assignments."roleId" = ? AND
23+
actors.type = 'singleUse'`, [roleId]);
24+
await db.raw(`DELETE FROM sessions WHERE "actorId" IN (
25+
SELECT id FROM actors
26+
JOIN assignments ON
27+
assignments."actorId" = actors.id AND
28+
assignments."roleId" = ?
29+
WHERE actors.type = 'singleUse'
30+
)`, [roleId]);
31+
await db.raw('DELETE FROM assignments WHERE "roleId" = ?', [roleId]);
32+
33+
// Delete the role assigned to backup creation tokens.
34+
await db.raw('DELETE FROM roles WHERE id = ?', [roleId]);
35+
};
36+
37+
const down = () => {};
38+
39+
module.exports = { up, down };

test/integration/other/migrations.js

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ const { readFileSync } = require('fs');
22
const appRoot = require('app-root-path');
33
const uuid = require('uuid/v4');
44
const config = require('config');
5-
const { testServiceFullTrx } = require('../setup');
5+
const { testContainerFullTrx, testServiceFullTrx } = require('../setup');
66
const { sql } = require('slonik');
77
// eslint-disable-next-line import/no-dynamic-require
8+
const { Actor, Config } = require(appRoot + '/lib/model/frames');
9+
// eslint-disable-next-line import/no-dynamic-require
810
const { withDatabase } = require(appRoot + '/lib/model/migrate');
911
const testData = require('../../data/xml');
1012
const populateUsers = require('../fixtures/01-users');
@@ -14,23 +16,19 @@ const { getFormFields } = require('../../../lib/data/schema');
1416

1517
const withTestDatabase = withDatabase(config.get('test.database'));
1618
const migrationsDir = appRoot + '/lib/model/migrations';
17-
const upToMigration = (toName) => withTestDatabase(async (migrator) => {
19+
const upToMigration = (toName, inclusive = true) => withTestDatabase(async (migrator) => {
1820
await migrator.raw('drop owned by current_user');
19-
// eslint-disable-next-line no-constant-condition
20-
while (true) {
21+
const migrations = await migrator.migrate.list({ directory: migrationsDir });
22+
const pending = migrations[1].map(({ file }) => file);
23+
const index = pending.indexOf(toName);
24+
if (index === -1) throw new Error(`Could not find migration ${toName}`);
25+
const end = inclusive ? index + 1 : index;
26+
for (let i = 0; i < end; i += 1)
2127
// eslint-disable-next-line no-await-in-loop
2228
await migrator.migrate.up({ directory: migrationsDir });
23-
// eslint-disable-next-line no-await-in-loop
24-
const migrations = await migrator.migrate.list({ directory: migrationsDir });
25-
const applied = migrations[0];
26-
const remaining = migrations[1];
27-
if (toName === applied[applied.length - 1]) break;
28-
if (remaining.length === 0) {
29-
// eslint-disable-next-line no-console
30-
console.log('Could not find migration', toName);
31-
break;
32-
}
33-
}
29+
// Confirm that the migrations completed as expected.
30+
const [completed] = await migrator.migrate.list({ directory: migrationsDir });
31+
completed.should.eql(pending.slice(0, end));
3432
});
3533
const up = () => withTestDatabase((migrator) =>
3634
migrator.migrate.up({ directory: migrationsDir }));
@@ -367,3 +365,98 @@ describe('datbase migrations: intermediate form schema', function() {
367365
res.count.should.equal(2); // only 2 schema IDs represented here
368366
}));
369367
});
368+
369+
// eslint-disable-next-line func-names, space-before-function-paren
370+
describe('database migrations: 20230123-01-remove-google-backups', function() {
371+
this.timeout(10000);
372+
373+
beforeEach(() => upToMigration('20230123-01-remove-google-backups.js', false));
374+
375+
it('deletes backups configs', testContainerFullTrx(async ({ Configs }) => {
376+
await Configs.set('backups.main', { a: 'b' });
377+
await Configs.set('backups.google', { c: 'd' });
378+
await Configs.set('analytics', { enabled: false });
379+
await up();
380+
(await Configs.get('backups.main')).isEmpty().should.be.true();
381+
(await Configs.get('backups.google')).isEmpty().should.be.true();
382+
(await Configs.get('analytics')).isDefined().should.be.true();
383+
}));
384+
385+
describe('backup creation token', () => {
386+
// Much of this was copied from the old endpoint
387+
// /v1/config/backups/initiate.
388+
const createToken = async ({ Actors, Assignments, Sessions }) => {
389+
const expiresAt = new Date();
390+
expiresAt.setHours(expiresAt.getHours() + 1);
391+
const actor = await Actors.create(new Actor({
392+
type: 'singleUse',
393+
displayName: 'Backup creation token',
394+
expiresAt,
395+
meta: {
396+
keys: { some: 'data' }
397+
}
398+
}));
399+
await Assignments.grantSystem(actor, 'initbkup', Config.species);
400+
await Sessions.create(actor);
401+
return actor.id;
402+
};
403+
404+
it('consumes a token', testContainerFullTrx(async (container) => {
405+
const actorId = await createToken(container);
406+
const { one } = container;
407+
const count = () => one(sql`SELECT
408+
(SELECT count(*) FROM actors WHERE id = ${actorId} AND "deletedAt" IS NOT NULL) AS "deletedActors",
409+
(SELECT count(*) FROM assignments WHERE "actorId" = ${actorId}) AS assignments,
410+
(SELECT count(*) FROM sessions WHERE "actorId" = ${actorId}) AS sessions`);
411+
(await count()).should.eql({
412+
deletedActors: 0,
413+
assignments: 1,
414+
sessions: 1
415+
});
416+
await up();
417+
(await count()).should.eql({
418+
deletedActors: 1,
419+
assignments: 0,
420+
sessions: 0
421+
});
422+
}));
423+
424+
it('does not modify other actor data', testServiceFullTrx(async (service, container) => {
425+
await populateUsers(container);
426+
await service.login('alice');
427+
await createToken(container);
428+
const { one } = container;
429+
const count = () => one(sql`SELECT
430+
(SELECT count(*) FROM actors where "deletedAt" is not null) AS "deletedActors",
431+
(SELECT count(*) FROM assignments) AS assignments,
432+
(SELECT count(*) FROM sessions) AS sessions`);
433+
(await count()).should.eql({
434+
deletedActors: 0,
435+
assignments: 3,
436+
sessions: 2
437+
});
438+
await up();
439+
// The counts decrease in the way that we would expect from the previous
440+
// test.
441+
(await count()).should.eql({
442+
deletedActors: 1,
443+
assignments: 2,
444+
sessions: 1
445+
});
446+
}));
447+
});
448+
449+
it('deletes the role that grants backup.verify', testContainerFullTrx(async ({ Roles }) => {
450+
const rolesBefore = await Roles.getAll();
451+
const canVerify = rolesBefore.filter(({ verbs }) =>
452+
verbs.includes('backup.verify'));
453+
canVerify.length.should.equal(1);
454+
canVerify[0].system.should.equal('initbkup');
455+
await up();
456+
const rolesAfter = await Roles.getAll();
457+
const deleted = rolesBefore.filter(roleBefore =>
458+
!rolesAfter.some(roleAfter => roleAfter.id === roleBefore.id));
459+
deleted.length.should.equal(1);
460+
deleted[0].system.should.equal('initbkup');
461+
}));
462+
});

0 commit comments

Comments
 (0)