Files
compras/scripts/install-mfa-pilot-schema.php
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

139 lines
8.8 KiB
PHP

<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Laravel\Fortify\Features;
use Symfony\Component\Process\Process;
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
if (! in_array('--apply-authorized', $argv, true)) {
throw new LogicException('Explicit preparation flag required.');
}
$s = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
if (($s['APP_ENV'] ?? '') !== 'payable-testing' || ($s['DB_HOST'] ?? '') !== '127.0.0.1' || realpath($s['HOST_ROOT'] ?? '') !== realpath($root) || ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $s['DB_DATABASE'] ?? '', $m) || $s['DB_USERNAME'] !== 'payable_qa_'.$m[1]) {
throw new LogicException('Wrong pilot destination.');
}
$app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$owner = DB::connection('pgsql')->getConfig();
unset($owner['name']);
if ($owner['host'] !== '127.0.0.1') {
throw new LogicException('Local server required.');
}
$qa = [...$owner, 'url' => null, 'database' => $s['DB_DATABASE'], 'username' => $s['DB_USERNAME'], 'password' => $s['DB_PASSWORD']];
config(['database.connections.receipt_install' => $qa, 'database.connections.receipt_owner' => [...$owner, 'url' => null, 'database' => 'postgres']]);
DB::setDefaultConnection('receipt_install');
$db = DB::connection();
$admin = DB::connection('receipt_owner');
$identity = $db->selectOne('select current_database() as db, current_user as role');
if ($identity->db !== $s['DB_DATABASE'] || $identity->role !== $s['DB_USERNAME']) {
throw new LogicException('Identity mismatch.');
}
$fingerprint = function ($connection, $tables) {
$out = [];
foreach ($tables as $table) {
$rows = $connection->select('select row_to_json(t)::text as row from "'.str_replace('"', '""', $table).'" t order by row_to_json(t)::text');
$out[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR));
}
return $out;
};
$pg = function ($binary, $args, $connection) {
$p = new Process(['C:/Program Files/PostgreSQL/17/bin/'.$binary.'.exe', ...$args], null, ['PGHOST' => $connection['host'], 'PGPORT' => (string) $connection['port'], 'PGUSER' => $connection['username'], 'PGPASSWORD' => $connection['password']], timeout: 120);
$p->run();
if (! $p->isSuccessful()) {
throw new RuntimeException('Backup/restore failed.');
}
};
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename');
$migration = 'database/migrations/2026_11_03_000001_add_two_factor_columns_to_users.php';
$proposalFile = $root.'/docs/proofs/mfa-pilot-schema-proposal.json';
$authorization = json_decode(ltrim(file_get_contents($root.'/docs/proofs/mfa-pilot-schema-authorization.json'), "\xEF\xBB\xBF"), true, 32, JSON_THROW_ON_ERROR);
$proposal = json_decode(ltrim(file_get_contents($proposalFile), "\xEF\xBB\xBF"), true, 32, JSON_THROW_ON_ERROR);
if ($authorization['proposal_sha256'] !== hash_file('sha256', $proposalFile) || $authorization['scope'] !== 'install_nullable_mfa_columns_only' || $authorization['environment'] !== 'http://127.0.0.1:8005' || $proposal['migration_sha256'] !== hash_file('sha256', $root.'/'.$migration) || file_exists($root.'/docs/proofs/mfa-pilot-schema-installation.json') || Features::enabled(Features::twoFactorAuthentication()) || filter_var($s['SGC_TWO_FACTOR_ENABLED'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
throw new LogicException('Wrong authorization, destination, feature state or already installed.');
}
foreach (['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'] as $column) {
if (Schema::connection('receipt_install')->hasColumn('users', $column)) {
throw new LogicException('MFA schema already exists; inspect before repeating.');
}
}
$restore = 'compras_pilot_restore_'.bin2hex(random_bytes(6));
$created = false;
try {
$db->beginTransaction();
$db->statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
$baseline = $fingerprint($db, $tables);
$directory = 'C:/laragon/data/sgf-tempo/backups/compras-mfa-installation-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3));
if (! mkdir($directory, 0700, true)) {
throw new RuntimeException('Backup unavailable.');
}
$dump = $directory.'/before.dump';
$snapshot = $db->selectOne('select pg_export_snapshot() as snapshot')->snapshot;
$pg('pg_dump', ['--format=custom', '--snapshot='.$snapshot, '--file='.$dump, '--dbname='.$qa['database']], $qa);
$db->commit();
$admin->statement('CREATE DATABASE "'.$restore.'"');
$created = true;
$pg('pg_restore', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner);
config(['database.connections.receipt_restore' => [...$owner, 'url' => null, 'database' => $restore]]);
$restored = DB::connection('receipt_restore');
if ($fingerprint($restored, $tables) !== $baseline) {
throw new LogicException('Restored backup differs.');
}
$columns = Schema::connection('receipt_restore')->getColumnListing('users');
$userRows = function ($connection) use ($columns) {
$selected = implode(',', array_map(fn ($c) => '"'.str_replace('"', '""', $c).'"', $columns));
return $connection->select('select row_to_json(t)::text as row from (select '.$selected.' from users) t order by row_to_json(t)::text');
};
$beforeUsers = $userRows($restored);
$migrationCount = $restored->table('migrations')->count();
if (Artisan::call('migrate', ['--database' => 'receipt_restore', '--path' => $migration, '--force' => true]) !== 0) {
throw new RuntimeException('Disposable migration failed.');
}
$protected = array_values(array_diff($tables, ['users', 'migrations']));
if ($fingerprint($restored, $protected) !== array_intersect_key($baseline, array_flip($protected)) || $userRows($restored) != $beforeUsers || $restored->table('migrations')->count() !== $migrationCount + 1) {
throw new LogicException('Unexpected migration changes.');
}
foreach (['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'] as $column) {
if ($restored->table('users')->whereNotNull($column)->exists()) {
throw new LogicException('Unexpected enrollment data.');
}
}
$db->transaction(function () use ($db, $tables, $baseline, $fingerprint, $migration, $protected, $userRows, $beforeUsers, $migrationCount) {
$db->statement("SET LOCAL lock_timeout = '5s'");
foreach ($tables as $table) {
$db->statement('LOCK TABLE "'.str_replace('"', '""', $table).'" IN SHARE MODE');
}
if ($fingerprint($db, $tables) !== $baseline) {
throw new LogicException('Pilot changed after backup; abort.');
}
if (Artisan::call('migrate', ['--database' => 'receipt_install', '--path' => $migration, '--force' => true]) !== 0) {
throw new RuntimeException('Pilot migration failed.');
}
if ($fingerprint($db, $protected) !== array_intersect_key($baseline, array_flip($protected)) || $userRows($db) != $beforeUsers || $db->table('migrations')->count() !== $migrationCount + 1) {
throw new LogicException('Pilot values changed unexpectedly.');
}
foreach (['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'] as $column) {
if ($db->table('users')->whereNotNull($column)->exists()) {
throw new LogicException('Fields not empty.');
}
}
});
$result = ['checked_at' => gmdate('c'), 'result' => 'passed', 'environment' => 'http://127.0.0.1:8005', 'scope' => 'install_nullable_mfa_columns_only', 'migration' => $migration, 'migration_sha256' => hash_file('sha256', $root.'/'.$migration), 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'existing_user_columns_preserved' => true, 'existing_users' => $restored->table('users')->count(), 'enrollment_created' => false, 'pilot_migration_applied' => true, 'pilot_configuration_changed' => false, 'passwords_or_grants_changed' => false, 'restore_database_removed' => false];
} finally {
if ($db->transactionLevel() > 0) {
$db->rollBack();
}
DB::purge('receipt_restore');
if ($created) {
$admin->statement('DROP DATABASE "'.$restore.'" WITH (FORCE)');
}
}
$result['restore_database_removed'] = true;
file_put_contents($root.'/docs/proofs/mfa-pilot-schema-installation.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
echo 'Three empty MFA fields installed; feature remains disabled.'.PHP_EOL;