ops: blindar reset preservando configuración estructural

This commit is contained in:
2026-09-05 22:17:45 -03:00
parent 06f809b4c8
commit 66333fb5dd
@@ -26,11 +26,7 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
const adminId = adminRows[0].id; const adminId = adminRows[0].id;
// Product configuration that must survive a clean operational start. const structuralTables = [
const preservedTables = new Set([
'typeorm_migrations',
'users',
'user_roles',
'roles', 'roles',
'permissions', 'permissions',
'role_permissions', 'role_permissions',
@@ -41,6 +37,34 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
'finding_catalog_items', 'finding_catalog_items',
'finding_catalog_item_asset_types', 'finding_catalog_item_asset_types',
'finding_catalog_asset_type_profiles', 'finding_catalog_asset_type_profiles',
];
// Snapshot structural row counts so TRUNCATE ... CASCADE can never silently
// remove product configuration while still leaving operational tables empty.
const structuralCounts = new Map<string, string>();
for (const table of structuralTables) {
const safeTable = `"${table.replace(/"/g, '""')}"`;
const rows: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM ${safeTable}`,
);
structuralCounts.set(table, rows[0]?.total ?? '0');
}
const adminRolesBefore: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM user_roles WHERE user_id = $1`,
[adminId],
);
const adminRoleCount = adminRolesBefore[0]?.total ?? '0';
if (adminRoleCount === '0') {
throw new Error('Production reset aborted: admin has no assigned role');
}
// Product configuration that must survive a clean operational start.
const preservedTables = new Set([
'typeorm_migrations',
'users',
'user_roles',
...structuralTables,
]); ]);
const tableRows: Array<{ table_name: string }> = await queryRunner.query(` const tableRows: Array<{ table_name: string }> = await queryRunner.query(`
@@ -68,14 +92,17 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
// A reset must invalidate every prior login token, including admin's. // A reset must invalidate every prior login token, including admin's.
// auth_sessions is operational and was truncated above; admin simply logs in again. // auth_sessions is operational and was truncated above; admin simply logs in again.
await queryRunner.query(` await queryRunner.query(
`
UPDATE users UPDATE users
SET failed_login_attempts = 0, SET failed_login_attempts = 0,
locked_until = NULL, locked_until = NULL,
last_login_at = NULL, last_login_at = NULL,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE id = $1
`, [adminId]); `,
[adminId],
);
const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(` const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(`
SELECT SELECT
@@ -88,6 +115,37 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
throw new Error('Production reset verification failed: users table is not admin-only'); throw new Error('Production reset verification failed: users table is not admin-only');
} }
const finalAdminRoles: Array<{ total: string; foreign_users: string }> = await queryRunner.query(
`
SELECT
count(*) FILTER (WHERE user_id = $1)::text AS total,
count(*) FILTER (WHERE user_id <> $1)::text AS foreign_users
FROM user_roles
`,
[adminId],
);
if (
finalAdminRoles[0]?.total !== adminRoleCount ||
finalAdminRoles[0]?.foreign_users !== '0'
) {
throw new Error('Production reset verification failed: admin role assignments changed');
}
// Assert every structural table kept exactly the same number of rows.
for (const table of structuralTables) {
const safeTable = `"${table.replace(/"/g, '""')}"`;
const rows: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM ${safeTable}`,
);
const before = structuralCounts.get(table) ?? '0';
if (rows[0]?.total !== before) {
throw new Error(
`Production reset verification failed: structural table ${table} changed (${before} -> ${rows[0]?.total ?? 'unknown'})`,
);
}
}
// Assert that every operational table is empty. This makes the migration // Assert that every operational table is empty. This makes the migration
// fail atomically if a table was repopulated during the reset transaction. // fail atomically if a table was repopulated during the reset transaction.
for (const table of operationalTables) { for (const table of operationalTables) {
@@ -103,7 +161,7 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
// Keep a concise server-side record in the migration log for deploy diagnostics. // Keep a concise server-side record in the migration log for deploy diagnostics.
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( console.log(
`[production-reset] kept admin=${adminRows[0].username} (${adminId}); cleared ${operationalTables.length} operational tables`, `[production-reset] kept admin=${adminRows[0].username} (${adminId}); preserved ${structuralTables.length} structural tables; cleared ${operationalTables.length} operational tables`,
); );
} }