70 lines
3.8 KiB
PHP
70 lines
3.8 KiB
PHP
<?php
|
|
|
|
// Rotate only the two pre-existing synthetic QA actors referenced by the resource HTTP fixture.
|
|
use App\Models\User;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
|
|
$root = dirname(__DIR__);
|
|
require $root.'/vendor/autoload.php';
|
|
require __DIR__.'/portal-qa-environment.php';
|
|
$target = portalQaEnvironment();
|
|
$app = require $root.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
portalQaAssertTarget($target);
|
|
$private = $root.'/storage/app/private/investmoz-v2/portal-audit';
|
|
$sessions = realpath($root.'/storage/app/private/investmoz-v2/qa-opportunity-resources-sessions');
|
|
$privateRoot = realpath($root.'/storage/app/private/investmoz-v2');
|
|
abort_unless($sessions && $privateRoot && str_starts_with($sessions, $privateRoot.DIRECTORY_SEPARATOR) && ! config('session.encrypt'), 422);
|
|
abort_unless(($argv[1] ?? '') === '--rotate', 422, 'Explicit QA-only rotation required.');
|
|
$actors = [];
|
|
$before = [];
|
|
foreach (['manager' => 'opportunity-manager@example.invalid', 'outsider' => 'investor-path-outsider-qa@example.invalid'] as $role => $email) {
|
|
$user = User::where('email', $email)->firstOrFail();
|
|
$before[$role] = $user->getRawOriginal();
|
|
$actors[$role] = ['id' => $user->id, 'email' => $email, 'password' => Str::password(40),
|
|
'totp_secret' => $user->two_factor_secret ? Crypt::decryptString($user->two_factor_secret) : null];
|
|
}
|
|
$account18 = DB::table('users')->where('id', 18)->first();
|
|
$account18Hash = hash('sha256', json_encode($account18, JSON_THROW_ON_ERROR));
|
|
DB::transaction(function () use ($actors): void {
|
|
foreach ($actors as $actor) {
|
|
DB::table('users')->where('id', $actor['id'])->where('email', $actor['email'])
|
|
->update(['password' => Hash::make($actor['password']), 'remember_token' => Str::random(60), 'updated_at' => now()]);
|
|
}
|
|
});
|
|
$invalidated = 0;
|
|
foreach (glob($sessions.DIRECTORY_SEPARATOR.'*') ?: [] as $file) {
|
|
$path = realpath($file);
|
|
if (! $path || ! is_file($path) || ! str_starts_with($path, $sessions.DIRECTORY_SEPARATOR) || ! preg_match('/^[a-zA-Z0-9]{40}$/', basename($path))) {
|
|
continue;
|
|
}
|
|
$data = @unserialize(file_get_contents($path), ['allowed_classes' => false]);
|
|
if (! is_array($data)) {
|
|
continue;
|
|
}
|
|
foreach ($data as $key => $value) {
|
|
if ((str_starts_with($key, 'login_web_') || $key === 'login.id') && in_array((int) $value, array_column($actors, 'id'), true)) {
|
|
abort_unless(unlink($path), 422, 'QA session invalidation failed.');
|
|
$invalidated++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
foreach ($actors as $role => $actor) {
|
|
$after = DB::table('users')->where('id', $actor['id'])->first();
|
|
foreach (['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'] as $column) {
|
|
abort_unless($before[$role][$column] === $after->$column, 422, 'MFA preservation check failed.');
|
|
}
|
|
abort_unless(! hash_equals($before[$role]['password'], $after->password), 422);
|
|
}
|
|
abort_unless($account18Hash === hash('sha256', json_encode(DB::table('users')->where('id', 18)->first(), JSON_THROW_ON_ERROR)), 422, 'Unrelated investor preservation check failed.');
|
|
file_put_contents($private.'/resource-http-actors-private.json', json_encode($actors, JSON_THROW_ON_ERROR));
|
|
$proof = ['captured_at' => gmdate('c'), 'target' => $target, 'rotated_synthetic_actors' => count($actors), 'qa_sessions_invalidated' => $invalidated,
|
|
'remember_tokens_rotated' => true, 'mfa_preserved' => true, 'investor_18_preserved' => true, 'active_writes' => 0, 'credentials_exported' => false];
|
|
file_put_contents($private.'/resource-http-actors-rotation-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode($proof, JSON_THROW_ON_ERROR).PHP_EOL;
|