Add owner-only manual database integrity check
This commit is contained in:
1 parent
ebfae0f803
commit
ddc303542a
8 files changed
+162
-21
No files matched your search
@@ -13,7 +13,7 @@ void test('admin HTTP denies anonymous, forged admin, room-host and spoofed iden
|
||||
assert.ok(cookie);
|
||||
const room = (await created.json() as { code: string }).code;
|
||||
for (const headers of [{}, { cookie }, { cookie: 'dune_admin_session=' + 'a'.repeat(64) }, { 'oai-authenticated-user-id': 'owner', 'oai-authenticated-user-email': 'owner@example.test' }] as Record<string, string>[]) {
|
||||
for (const path of ['/api/admin/session', '/api/admin/accounts', '/api/admin/audit', '/api/admin/rooms', `/api/admin/rooms/${room}/control`, `/api/admin/rooms/${room}/lobby`, `/api/admin/rooms/${room}/removal`, `/api/admin/rooms/${room}/closure`, `/api/admin/rooms/${room}/archive`, `/api/admin/rooms/${room}/seat-ai`, `/api/admin/rooms/${room}/discussion`]) {
|
||||
for (const path of ['/api/admin/session', '/api/admin/accounts', '/api/admin/audit', '/api/admin/operations?integrity=1', '/api/admin/rooms', `/api/admin/rooms/${room}/control`, `/api/admin/rooms/${room}/lobby`, `/api/admin/rooms/${room}/removal`, `/api/admin/rooms/${room}/closure`, `/api/admin/rooms/${room}/archive`, `/api/admin/rooms/${room}/seat-ai`, `/api/admin/rooms/${room}/discussion`]) {
|
||||
const result = await fetch(base + path, { headers, signal: AbortSignal.timeout(15000) });
|
||||
assert.equal(result.status, 401, path);
|
||||
assert.equal(result.headers.get('cache-control'), 'no-store');
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
import { adminLogin, requireAdmin, AdminError } from '../db/admin-access';
|
||||
import { readAdminOperations } from '../db/admin-operations';
|
||||
import { readAdminIntegrity, readAdminOperations } from '../db/admin-operations';
|
||||
import { adminStore } from './admin-access-fixture';
|
||||
|
||||
const now = 10_000;
|
||||
@@ -35,6 +35,9 @@ void test('owner operations sample counts saved states and backup provenance wit
|
||||
});
|
||||
assert.equal(JSON.stringify(result).includes('secret'), false);
|
||||
assert.deepEqual(store.sqlite.prepare('SELECT code,state,version FROM rooms ORDER BY code').all(), before);
|
||||
assert.deepEqual(await readAdminIntegrity(store.database, identity, now),
|
||||
{ observedAt: now, sqlite: 'passed', foreignKeys: 'passed' });
|
||||
assert.deepEqual(store.sqlite.prepare('SELECT code,state,version FROM rooms ORDER BY code').all(), before);
|
||||
} finally { store.sqlite.close(); }
|
||||
});
|
||||
|
||||
@@ -53,3 +56,40 @@ void test('stale owner identity, demotion and revoked session cannot sample oper
|
||||
await assert.rejects(readAdminOperations(store.database, identity, now), denied);
|
||||
} finally { store.sqlite.close(); }
|
||||
});
|
||||
|
||||
void test('manual integrity check detects orphaned foreign keys without exposing their rows', async () => {
|
||||
const store = adminStore();
|
||||
try {
|
||||
const owner = store.provision('owner');
|
||||
const login = await adminLogin(store.database, owner.key, now);
|
||||
const identity = await requireAdmin(store.database, login.token, ['owner'], now);
|
||||
store.sqlite.exec('PRAGMA foreign_keys=OFF');
|
||||
store.sqlite.prepare('INSERT INTO seats(token_hash,room_code,player_id,revoked) VALUES(?,?,?,0)')
|
||||
.run('private-orphan-token', 'GHOSTABC', 'private-player');
|
||||
store.sqlite.exec('PRAGMA foreign_keys=ON');
|
||||
const before = store.sqlite.prepare('SELECT * FROM seats').all();
|
||||
const result = await readAdminIntegrity(store.database, identity, now);
|
||||
assert.deepEqual(result, { observedAt: now, sqlite: 'passed', foreignKeys: 'failed' });
|
||||
assert.equal(JSON.stringify(result).includes('private'), false);
|
||||
assert.deepEqual(store.sqlite.prepare('SELECT * FROM seats').all(), before);
|
||||
store.provision('owner', 'Backup owner');
|
||||
store.sqlite.prepare("UPDATE admin_accounts SET role='operator' WHERE id=?").run(owner.id);
|
||||
await assert.rejects(readAdminIntegrity(store.database, identity, now),
|
||||
(error: unknown) => error instanceof AdminError && error.status === 403);
|
||||
} finally { store.sqlite.close(); }
|
||||
});
|
||||
|
||||
void test('manual integrity check revalidates owner authority before returning results', async () => {
|
||||
const store = adminStore();
|
||||
try {
|
||||
const owner = store.provision('owner');
|
||||
store.provision('owner', 'Remaining owner');
|
||||
const login = await adminLogin(store.database, owner.key, now);
|
||||
const identity = await requireAdmin(store.database, login.token, ['owner'], now);
|
||||
store.hooks.beforeBatch = async () => {
|
||||
store.sqlite.prepare("UPDATE admin_accounts SET role='viewer' WHERE id=?").run(owner.id);
|
||||
};
|
||||
await assert.rejects(readAdminIntegrity(store.database, identity, now),
|
||||
(error: unknown) => error instanceof AdminError && error.status === 403);
|
||||
} finally { store.sqlite.close(); }
|
||||
});
|
||||
Reference in new issue
Block a user