Connect personal operator-provisioned admin keys, persisted roles and eight-hour sessions, current/all-session revocation and a filtered room directory. Additive schema protects final owner, revokes disabled accounts and records safe audit history. No game state changes; remaining administration scope stays explicit. Verified: typecheck, lint, 5196 offline tests, 52 HTTP tests, production build; authenticated local HTTP and desktop/mobile browser checks. Independent privacy/security review fixed malformed nested roster searches and read-time session revocation. Preserved 1,671 prior rooms and 3,538 seats. Container/deployed acceptance follows publication.
228 lines
7.6 KiB
TypeScript
228 lines
7.6 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { adminLogin, requireAdmin } from '../db/admin-access';
|
|
import { adminStore, sha256 } from './admin-access-fixture';
|
|
|
|
const checkout = fileURLToPath(new URL('../', import.meta.url));
|
|
const cli = fileURLToPath(
|
|
new URL('../tools/admin-access.mjs', import.meta.url),
|
|
);
|
|
const run = (...args: string[]) =>
|
|
spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' });
|
|
|
|
void test('operator CLI writes a unique private key outside the checkout and SQL provisions with safely escaped names', async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-admin-cli-'));
|
|
const output = join(directory, 'owner');
|
|
const store = adminStore();
|
|
try {
|
|
assert.ok(store.sqlite instanceof DatabaseSync);
|
|
const name = "O'Brien'); DROP TABLE rooms; --";
|
|
const result = run('--name', name, '--role', 'owner', '--out', output);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(readdirSync(output).sort(), [
|
|
'access-key.txt',
|
|
'provision.sql',
|
|
]);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const file of ['access-key.txt', 'provision.sql'])
|
|
assert.equal(statSync(join(output, file)).mode & 0o777, 0o600);
|
|
const key = readFileSync(join(output, 'access-key.txt'), 'utf8').trim();
|
|
const sql = readFileSync(join(output, 'provision.sql'), 'utf8');
|
|
assert.match(key, /^dune-admin\.[0-9a-f-]{36}\.[0-9a-f]{64}$/);
|
|
assert.equal(sql.includes(key), false);
|
|
assert.equal(sql.includes(key.split('.').at(-1)!), false);
|
|
assert.equal(sql.includes(sha256(key)), true);
|
|
assert.equal((result.stdout + result.stderr).includes(key), false);
|
|
assert.equal((result.stdout + result.stderr).includes(sha256(key)), false);
|
|
store.sqlite.exec(sql);
|
|
assert.equal(
|
|
store.sqlite.prepare('SELECT name FROM admin_accounts').get()!.name,
|
|
name,
|
|
);
|
|
assert.equal(
|
|
store.sqlite.prepare('SELECT count(*) AS n FROM rooms').get()!.n,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
store.sqlite
|
|
.prepare(
|
|
"SELECT count(*) AS n FROM admin_audit WHERE action = 'provision'",
|
|
)
|
|
.get()!.n,
|
|
1,
|
|
);
|
|
const login = await adminLogin(store.database, key, 1000);
|
|
await requireAdmin(store.database, login.token, ['owner'], 2000);
|
|
assert.throws(() => store.sqlite.exec(sql), /UNIQUE constraint/);
|
|
assert.equal(
|
|
store.sqlite
|
|
.prepare(
|
|
"SELECT count(*) AS n FROM admin_audit WHERE action = 'provision'",
|
|
)
|
|
.get()!.n,
|
|
1,
|
|
);
|
|
} finally {
|
|
store.sqlite.close();
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
void test('operator CLI revocation needs only a safe account ID, revokes sessions and refuses the final enabled owner', async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-admin-revoke-cli-'));
|
|
const store = adminStore();
|
|
try {
|
|
const first = store.provision();
|
|
const second = store.provision();
|
|
const login = await adminLogin(store.database, first.key, 1000);
|
|
const output = join(directory, 'revoke');
|
|
const result = run('--revoke', first.id, '--out', output);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(readdirSync(output), ['revoke.sql']);
|
|
const sql = readFileSync(join(output, 'revoke.sql'), 'utf8');
|
|
assert.equal(
|
|
(sql + result.stdout + result.stderr).includes(first.key),
|
|
false,
|
|
);
|
|
assert.equal(sql.includes('key_hash'), false);
|
|
store.sqlite.exec(sql);
|
|
await assert.rejects(
|
|
requireAdmin(store.database, login.token, undefined, 2000),
|
|
);
|
|
await assert.rejects(adminLogin(store.database, first.key, 2000));
|
|
assert.equal(
|
|
store.sqlite
|
|
.prepare(
|
|
"SELECT count(*) AS n FROM admin_audit WHERE action = 'revoke'",
|
|
)
|
|
.get()!.n,
|
|
1,
|
|
);
|
|
store.sqlite.exec(sql);
|
|
assert.equal(
|
|
store.sqlite
|
|
.prepare(
|
|
"SELECT count(*) AS n FROM admin_audit WHERE action = 'revoke'",
|
|
)
|
|
.get()!.n,
|
|
1,
|
|
);
|
|
const lastOutput = join(directory, 'last');
|
|
assert.equal(run('--revoke', second.id, '--out', lastOutput).status, 0);
|
|
assert.throws(
|
|
() =>
|
|
store.sqlite.exec(readFileSync(join(lastOutput, 'revoke.sql'), 'utf8')),
|
|
/final enabled administrator owner/,
|
|
);
|
|
assert.equal(
|
|
store.sqlite
|
|
.prepare('SELECT enabled FROM admin_accounts WHERE id = ?')
|
|
.get(second.id)!.enabled,
|
|
1,
|
|
);
|
|
} finally {
|
|
store.sqlite.close();
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
void test('operator CLI refuses existing paths, checkout paths and symlink aliases without overwriting files', () => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-admin-paths-'));
|
|
const inside = join(checkout, '.admin-test-output');
|
|
try {
|
|
assert.equal(existsSync(inside), false);
|
|
const direct = run('--name', 'Owner', '--role', 'owner', '--out', inside);
|
|
assert.equal(direct.status, 1);
|
|
assert.equal(existsSync(inside), false);
|
|
const alias = join(directory, 'checkout-alias');
|
|
symlinkSync(checkout, alias, 'dir');
|
|
const viaAlias = run(
|
|
'--name',
|
|
'Owner',
|
|
'--role',
|
|
'owner',
|
|
'--out',
|
|
join(alias, '.admin-test-output'),
|
|
);
|
|
assert.equal(viaAlias.status, 1);
|
|
assert.equal(existsSync(inside), false);
|
|
const existing = join(directory, 'existing');
|
|
mkdirSync(existing, { mode: 0o700 });
|
|
writeFileSync(join(existing, 'access-key.txt'), 'existing private value', {
|
|
mode: 0o600,
|
|
});
|
|
const reuse = run('--name', 'Owner', '--role', 'owner', '--out', existing);
|
|
assert.equal(reuse.status, 1);
|
|
assert.equal(
|
|
readFileSync(join(existing, 'access-key.txt'), 'utf8'),
|
|
'existing private value',
|
|
);
|
|
assert.equal(
|
|
(reuse.stdout + reuse.stderr).includes('existing private value'),
|
|
false,
|
|
);
|
|
assert.deepEqual(readdirSync(existing), ['access-key.txt']);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
void test('operator CLI rejects malformed names, roles, IDs and incomplete commands before generating any credential', () => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-admin-input-'));
|
|
try {
|
|
const output = join(directory, 'new');
|
|
for (const args of [
|
|
[],
|
|
['--name', 'Owner', '--out', output],
|
|
['--name', 'Owner', '--role', 'superuser', '--out', output],
|
|
['--name', ' ', '--role', 'owner', '--out', output],
|
|
['--name', 'Owner\nInjected', '--role', 'owner', '--out', output],
|
|
['--name', 'x'.repeat(81), '--role', 'owner', '--out', output],
|
|
[
|
|
'--name',
|
|
'Owner',
|
|
'--role',
|
|
'owner',
|
|
'--role',
|
|
'viewer',
|
|
'--out',
|
|
output,
|
|
],
|
|
['--name', 'Owner', '--role', 'owner', '--out', 'relative-output'],
|
|
['--revoke', "x' OR 1=1;--", '--out', output],
|
|
[
|
|
'--revoke',
|
|
'10000000-0000-4000-8000-000000000001',
|
|
'--name',
|
|
'Owner',
|
|
'--out',
|
|
output,
|
|
],
|
|
]) {
|
|
assert.equal(run(...args).status, 1);
|
|
assert.equal(existsSync(output), false);
|
|
}
|
|
const help = run('--help');
|
|
assert.equal(help.status, 0);
|
|
assert.match(help.stdout, /No database is contacted/);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|