fix: accept audit harness tenant rehome CLI aliases
This commit is contained in:
parent
c73bb3b807
commit
d52ecf3261
4 changed files with 42 additions and 13 deletions
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
`scripts/rehome-tenant.mjs` переносит все строки из одного tenant ID в другой в схеме `public`. Скрипт рассчитан на остановленное приложение и запускается только привилегированной ролью БД, способной обходить RLS и изменять таблицы. Строки пароля или подключения в отчёты не выводятся.
|
||||
|
||||
Сначала на восстановленной копии базы выполните dry-run:
|
||||
CLI по умолчанию выполняет только read-only dry-run; `--dry-run` можно указать явно. Совместимые имена `--from`/`--to` и `--json` принимаются для rehearsal-обвязок; вывод всегда JSON. Сначала на восстановленной копии базы выполните dry-run:
|
||||
|
||||
```powershell
|
||||
$env:DATABASE_REHOME_URL = 'postgresql://…'
|
||||
node scripts/rehome-tenant.mjs --dry-run --source <ULID> --target <ULID>
|
||||
$env:DATABASE_ADMIN_URL = 'postgresql://…'
|
||||
node scripts/rehome-tenant.mjs --from <ULID> --to <ULID> --dry-run --json
|
||||
```
|
||||
|
||||
Для совместимости со старыми локальными инструкциями также принимаются `DATABASE_REHOME_URL` и `DATABASE_MIGRATION_URL` как fallback. `--apply` по-прежнему требует явный флаг, actor и проверенный `--backup-evidence`; aliases и default dry-run не разрешают запись сами по себе.
|
||||
|
||||
Dry-run открывает транзакцию `READ ONLY` и выводит только идентификаторы переноса и количество строк по таблицам. Он не блокирует записи; поэтому результат является снимком для планирования, а не гарантией актуальности на момент apply. На копии и в согласованное окно с остановленным приложением повторите dry-run непосредственно перед переносом.
|
||||
|
||||
## Обязательное подтверждение восстановления
|
||||
|
|
|
|||
|
|
@ -35,7 +35,16 @@ export interface RehomeReport {
|
|||
payrollLedger?: Array<{ currency: string; amountMinor: string }>;
|
||||
}
|
||||
|
||||
export function parseTenantRehomeArgs(args: string[]): Record<string, string>;
|
||||
export interface ParsedTenantRehomeArgs {
|
||||
mode: 'dry-run' | 'apply';
|
||||
source: string;
|
||||
target: string;
|
||||
actor?: string;
|
||||
backup_evidence?: string;
|
||||
json?: true;
|
||||
}
|
||||
|
||||
export function parseTenantRehomeArgs(args: string[]): ParsedTenantRehomeArgs;
|
||||
export function validateBackupEvidenceObject(
|
||||
evidence: unknown,
|
||||
): BackupEvidenceReport;
|
||||
|
|
|
|||
|
|
@ -47,11 +47,21 @@ export function parseTenantRehomeArgs(args) {
|
|||
if (key === '--dry-run' || key === '--apply') {
|
||||
if (result.mode) throw new SafeOperationError('Choose exactly one of --dry-run or --apply.');
|
||||
result.mode = key.slice(2);
|
||||
} else if (['--source', '--target', '--actor', '--backup-evidence'].includes(key)) {
|
||||
} else if (key === '--json') {
|
||||
if (result.json) throw new SafeOperationError('Duplicate option --json.');
|
||||
result.json = true;
|
||||
} else if (['--source', '--from', '--target', '--to', '--actor', '--backup-evidence'].includes(key)) {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith('--')) throw new SafeOperationError(`Missing value for ${key}.`);
|
||||
const name = key.slice(2).replaceAll('-', '_');
|
||||
if (result[name]) throw new SafeOperationError(`Duplicate option ${key}.`);
|
||||
const name = ({
|
||||
'--source': 'source',
|
||||
'--from': 'source',
|
||||
'--target': 'target',
|
||||
'--to': 'target',
|
||||
'--actor': 'actor',
|
||||
'--backup-evidence': 'backup_evidence',
|
||||
})[key];
|
||||
if (result[name]) throw new SafeOperationError(`Duplicate option for ${name.replaceAll('_', '-')}.`);
|
||||
result[name] = value;
|
||||
index += 1;
|
||||
} else {
|
||||
|
|
@ -59,7 +69,7 @@ export function parseTenantRehomeArgs(args) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!result.mode) throw new SafeOperationError('Specify --dry-run or --apply.');
|
||||
result.mode ??= 'dry-run';
|
||||
for (const name of ['source', 'target']) {
|
||||
if (!ULID_RE.test(result[name] ?? '')) throw new SafeOperationError(`--${name} must be a valid ULID.`);
|
||||
}
|
||||
|
|
@ -362,8 +372,8 @@ export async function rehomeTenant(client, options) {
|
|||
|
||||
function parseCli(args) {
|
||||
const options = parseTenantRehomeArgs(args);
|
||||
const connectionString = process.env.DATABASE_REHOME_URL ?? process.env.DATABASE_MIGRATION_URL;
|
||||
if (!connectionString) throw new SafeOperationError('Set DATABASE_REHOME_URL or DATABASE_MIGRATION_URL.');
|
||||
const connectionString = process.env.DATABASE_ADMIN_URL ?? process.env.DATABASE_REHOME_URL ?? process.env.DATABASE_MIGRATION_URL;
|
||||
if (!connectionString) throw new SafeOperationError('Set DATABASE_ADMIN_URL, DATABASE_REHOME_URL, or DATABASE_MIGRATION_URL.');
|
||||
return { ...options, connectionString };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,18 @@ const actor = '01J00000000000000000000003';
|
|||
const sha256 = 'a'.repeat(64);
|
||||
|
||||
describe('tenant rehome argument and evidence validation', () => {
|
||||
it('requires explicit apply mode and a backup evidence path', () => {
|
||||
expect(() => parseTenantRehomeArgs(['--source', source, '--target', target])).toThrow('--dry-run or --apply');
|
||||
expect(() => parseTenantRehomeArgs(['--apply', '--source', source, '--target', target, '--actor', actor]))
|
||||
it('defaults to read-only dry-run and accepts the published CLI aliases', () => {
|
||||
expect(parseTenantRehomeArgs(['--from', source, '--to', target, '--json'])).toEqual({
|
||||
mode: 'dry-run', source, target, json: true,
|
||||
});
|
||||
expect(parseTenantRehomeArgs(['--dry-run', '--source', source, '--target', target]).mode).toBe('dry-run');
|
||||
});
|
||||
|
||||
it('requires backup evidence for every apply invocation', () => {
|
||||
expect(() => parseTenantRehomeArgs(['--apply', '--from', source, '--to', target, '--actor', actor]))
|
||||
.toThrow('--backup-evidence is required');
|
||||
expect(() => parseTenantRehomeArgs(['--from', source, '--source', source, '--to', target]))
|
||||
.toThrow('Duplicate option for source.');
|
||||
});
|
||||
|
||||
it('accepts only evidence with encrypted off-host backup and a matching successful restore', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue