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 и изменять таблицы. Строки пароля или подключения в отчёты не выводятся.
|
`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
|
```powershell
|
||||||
$env:DATABASE_REHOME_URL = 'postgresql://…'
|
$env:DATABASE_ADMIN_URL = 'postgresql://…'
|
||||||
node scripts/rehome-tenant.mjs --dry-run --source <ULID> --target <ULID>
|
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 непосредственно перед переносом.
|
Dry-run открывает транзакцию `READ ONLY` и выводит только идентификаторы переноса и количество строк по таблицам. Он не блокирует записи; поэтому результат является снимком для планирования, а не гарантией актуальности на момент apply. На копии и в согласованное окно с остановленным приложением повторите dry-run непосредственно перед переносом.
|
||||||
|
|
||||||
## Обязательное подтверждение восстановления
|
## Обязательное подтверждение восстановления
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,16 @@ export interface RehomeReport {
|
||||||
payrollLedger?: Array<{ currency: string; amountMinor: string }>;
|
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(
|
export function validateBackupEvidenceObject(
|
||||||
evidence: unknown,
|
evidence: unknown,
|
||||||
): BackupEvidenceReport;
|
): BackupEvidenceReport;
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,21 @@ export function parseTenantRehomeArgs(args) {
|
||||||
if (key === '--dry-run' || key === '--apply') {
|
if (key === '--dry-run' || key === '--apply') {
|
||||||
if (result.mode) throw new SafeOperationError('Choose exactly one of --dry-run or --apply.');
|
if (result.mode) throw new SafeOperationError('Choose exactly one of --dry-run or --apply.');
|
||||||
result.mode = key.slice(2);
|
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];
|
const value = args[index + 1];
|
||||||
if (!value || value.startsWith('--')) throw new SafeOperationError(`Missing value for ${key}.`);
|
if (!value || value.startsWith('--')) throw new SafeOperationError(`Missing value for ${key}.`);
|
||||||
const name = key.slice(2).replaceAll('-', '_');
|
const name = ({
|
||||||
if (result[name]) throw new SafeOperationError(`Duplicate option ${key}.`);
|
'--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;
|
result[name] = value;
|
||||||
index += 1;
|
index += 1;
|
||||||
} else {
|
} 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']) {
|
for (const name of ['source', 'target']) {
|
||||||
if (!ULID_RE.test(result[name] ?? '')) throw new SafeOperationError(`--${name} must be a valid ULID.`);
|
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) {
|
function parseCli(args) {
|
||||||
const options = parseTenantRehomeArgs(args);
|
const options = parseTenantRehomeArgs(args);
|
||||||
const connectionString = process.env.DATABASE_REHOME_URL ?? process.env.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_REHOME_URL or DATABASE_MIGRATION_URL.');
|
if (!connectionString) throw new SafeOperationError('Set DATABASE_ADMIN_URL, DATABASE_REHOME_URL, or DATABASE_MIGRATION_URL.');
|
||||||
return { ...options, connectionString };
|
return { ...options, connectionString };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,18 @@ const actor = '01J00000000000000000000003';
|
||||||
const sha256 = 'a'.repeat(64);
|
const sha256 = 'a'.repeat(64);
|
||||||
|
|
||||||
describe('tenant rehome argument and evidence validation', () => {
|
describe('tenant rehome argument and evidence validation', () => {
|
||||||
it('requires explicit apply mode and a backup evidence path', () => {
|
it('defaults to read-only dry-run and accepts the published CLI aliases', () => {
|
||||||
expect(() => parseTenantRehomeArgs(['--source', source, '--target', target])).toThrow('--dry-run or --apply');
|
expect(parseTenantRehomeArgs(['--from', source, '--to', target, '--json'])).toEqual({
|
||||||
expect(() => parseTenantRehomeArgs(['--apply', '--source', source, '--target', target, '--actor', actor]))
|
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');
|
.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', () => {
|
it('accepts only evidence with encrypted off-host backup and a matching successful restore', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue