61 lines
3.3 KiB
PHP
61 lines
3.3 KiB
PHP
<?php
|
|
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tempo\Purchasing\Delivery\Installation;
|
|
use Tempo\Purchasing\Delivery\SqlPrivileges;
|
|
|
|
require getenv('FIX_CLONE').'/vendor/autoload.php';
|
|
$app = require getenv('FIX_CLONE').'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$bundle = getenv('FIX_BUNDLE');
|
|
require $bundle.'/integrations/sgf/operational/bootstrap.php';
|
|
$db = DB::connection();
|
|
$runtime = getenv('FIX_RUNTIME');
|
|
if (! preg_match('/^sgf_reproduction_restore_[a-f0-9]{12}$/D', $db->getDatabaseName()) || ! preg_match('/^fix_app_[a-f0-9]{12}$/D', $runtime)) {
|
|
throw new RuntimeException('Isolated recovery target required.');
|
|
}
|
|
$baseline = json_decode(file_get_contents(getenv('FIX_WORK').'/baseline.json'), true, 64, JSON_THROW_ON_ERROR);
|
|
$q = fn ($name) => '"'.str_replace('"', '""', $name).'"';
|
|
$db->statement('GRANT USAGE ON SCHEMA public TO '.$q($runtime));
|
|
foreach ($baseline['runtime_table_acl'] as $table => $rights) {
|
|
if ($rights) {
|
|
$db->statement('GRANT '.implode(', ', $rights).' ON TABLE public.'.$q($table).' TO '.$q($runtime));
|
|
}
|
|
if (in_array('INSERT', $rights, true) && $db->getSchemaBuilder()->hasColumn($table, 'id')) {
|
|
$seq = $db->selectOne('select pg_get_serial_sequence(?,?) as name', ['public.'.$table, 'id'])->name;
|
|
if ($seq) {
|
|
$db->statement('GRANT USAGE ON SEQUENCE '.$seq.' TO '.$q($runtime));
|
|
}
|
|
}
|
|
}
|
|
$db->statement('REVOKE ALL ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC');
|
|
foreach ($baseline['runtime_functions'] as $function) {
|
|
$db->statement('GRANT EXECUTE ON FUNCTION '.$function.' TO '.$q($runtime));
|
|
}
|
|
foreach (SqlPrivileges::plan($runtime)['statements'] as $sql) {
|
|
$db->statement($sql);
|
|
}
|
|
$expected = json_decode(file_get_contents(getenv('FIX_WORK').'/expected.json'), true, 64, JSON_THROW_ON_ERROR);
|
|
$fingerprints = [];
|
|
foreach (array_keys($expected) as $table) {
|
|
$rows = $db->select('select to_jsonb(t)::text as data from "'.$table.'" t order by to_jsonb(t)::text');
|
|
$fingerprints[$table] = hash('sha256', json_encode(array_column($rows, 'data'), JSON_THROW_ON_ERROR));
|
|
}
|
|
if ($fingerprints !== $expected) {
|
|
throw new RuntimeException('Recovered data differs.');
|
|
}
|
|
config(['database.connections.recovered_runtime' => [...$db->getConfig(), 'username' => $runtime, 'password' => getenv('FIX_RUNTIME_PASSWORD'), 'url' => null]]);
|
|
$rt = DB::connection('recovered_runtime');
|
|
if ($rt->table('ir_requests')->value('status') !== 'approved' || $rt->table('cb_budget_entries')->where('kind', 'reserve')->count() !== 1) {
|
|
throw new RuntimeException('Recovered runtime cannot consult original decision or reservation.');
|
|
}
|
|
if ($rt->selectOne("select has_schema_privilege(current_user,'public','CREATE') as allowed")->allowed) {
|
|
throw new RuntimeException('Recovery escalated runtime role.');
|
|
}
|
|
$receipt = $rt->table('sgc_installations')->first();
|
|
if ($receipt->schema_signature !== Installation::signature($rt, Installation::tables())) {
|
|
throw new RuntimeException('Recovered schema differs.');
|
|
}
|
|
echo json_encode(['verified' => true, 'tables_reconciled' => count($fingerprints), 'approved_origin_reservation_versions_preserved' => true, 'runtime_read_verified' => true, 'runtime_ddl_denied' => true, 'schema_and_triggers_verified' => true, 'sql_acl_reapplied_only_to_isolated_role' => true], JSON_THROW_ON_ERROR);
|