decrypt($user->two_factor_secret, false); abort_unless(is_string($plain), 422, 'Configuração MFA inválida.'); if (preg_match('/^s:[0-9]+:"([A-Z2-7]+=*)";$/D', $plain, $matches) && serialize($matches[1]) === $plain) { return $matches[1]; } return $plain; } public function validateBootstrapHolder(User $user): void { abort_unless($user->hasVerifiedEmail() && $user->two_factor_secret && $user->two_factor_confirmed_at, 422, 'Conta confirmada com MFA necessário.'); try { $secret = $this->mfaSecret($user); } catch (DecryptException) { abort(422, 'Configuração MFA incompatível com este ambiente. Recuperar pelo procedimento existente.'); } abort_unless(strlen($secret) >= 16 && preg_match('/^[A-Z2-7]+=*$/', $secret), 422, 'Configuração MFA inválida.'); abort_if(DB::table('portal_role_assignments')->where('user_id', $user->id)->where('role', 'auditor')->whereNull('revoked_at') ->where('starts_at', '<=', now())->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->exists(), 422, 'A restrição do Auditor impede a designação nesta cerimónia.'); } public function initialize(User $first, User $second, User $confirmer, string $approvalId): void { abort_unless(config('portal_profiles.bootstrap_authorized') === true, 403); abort_unless($first->id !== $second->id && $confirmer->id === $second->id, 403, 'Confirmação pelo segundo titular, distinta do pedido.'); foreach ([$first, $second] as $user) $this->validateBootstrapHolder($user); abort_unless($this->access->mfa($confirmer), 403); DB::transaction(function () use ($first, $second, $confirmer, $approvalId) { DB::select('SELECT pg_advisory_xact_lock(101026, 40001)'); $approval = DB::table('portal_security_approvals')->where('id', $approvalId)->lockForUpdate()->first(); abort_unless($approval && $approval->kind === 'bootstrap' && (int) $approval->requested_by === $first->id && (int) $approval->subject_user_id === $second->id && (int) $approval->approved_by === $confirmer->id && $approval->consumed_at === null && strtotime($approval->expires_at) > time(), 409, 'Pedido cruzado inválido, expirado ou já consumido.'); abort_if(DB::table('portal_superuser_pair')->where('id', 1)->exists(), 409, 'Dupla existente; use substituição atómica.'); DB::table('portal_superuser_pair')->insert(['id' => 1, 'first_user_id' => $first->id, 'second_user_id' => $second->id, 'approved_by' => $confirmer->id, 'created_at' => now(), 'updated_at' => now()]); DB::table('portal_security_approvals')->where('id', $approvalId)->update(['consumed_at' => now(), 'updated_at' => now()]); $this->access->audit($confirmer, 'superuser_pair_initialized', ['approval_id' => $approvalId, 'requested_by' => $first->id, 'holders' => [$first->id, $second->id], 'version' => 1]); }); } public function requestReplacement(User $actor, int $oldId, int $newId, string $reason): string { $this->administration->administrative($actor); abort_unless(mb_strlen($reason) >= 20 && mb_strlen($reason) <= 1000 && $oldId !== $newId, 422); return DB::transaction(function () use ($actor, $oldId, $newId, $reason) { $pair = DB::table('portal_superuser_pair')->where('id', 1)->lockForUpdate()->first(); $holders = [(int) $pair->first_user_id, (int) $pair->second_user_id]; abort_unless(in_array($oldId, $holders, true) && ! in_array($newId, $holders, true), 422); $candidate = User::findOrFail($newId); abort_unless($candidate->hasVerifiedEmail() && $candidate->two_factor_secret && $candidate->two_factor_confirmed_at, 422); $id = (string) Str::uuid(); DB::table('portal_security_approvals')->insert(['id' => $id, 'kind' => 'replacement', 'requested_by' => $actor->id, 'subject_user_id' => $newId, 'scope' => json_encode(['old_user_id' => $oldId, 'pair_version' => (int) $pair->version], JSON_THROW_ON_ERROR), 'reason' => $reason, 'expires_at' => now()->addMinutes(15), 'created_at' => now(), 'updated_at' => now()]); $this->access->audit($actor, 'superuser_replacement_requested', ['approval_id' => $id, 'old_user_id' => $oldId, 'new_user_id' => $newId]); return $id; }); } public function approve(User $actor, string $id): void { $this->administration->administrative($actor); DB::transaction(function () use ($actor, $id) { $row = DB::table('portal_security_approvals')->where('id', $id)->lockForUpdate()->first(); abort_unless($row !== null, 404); abort_unless(in_array($row->kind, ['replacement', 'elevation'], true), 403, 'Use a confirmação específica da inicialização cruzada.'); abort_if((int) $row->requested_by === $actor->id || (int) $row->subject_user_id === $actor->id, 403, 'Aprovação independente necessária.'); abort_unless($row->consumed_at === null && strtotime($row->expires_at) > time() && $row->approved_by === null, 409); DB::table('portal_security_approvals')->where('id', $id)->update(['approved_by' => $actor->id, 'updated_at' => now()]); $this->access->audit($actor, 'security_operation_approved', ['approval_id' => $id, 'kind' => $row->kind]); }); } public function applyReplacement(User $actor, string $id): void { $this->administration->administrative($actor); DB::transaction(function () use ($actor, $id) { DB::select('SELECT pg_advisory_xact_lock(101026, 40001)'); $pair = DB::table('portal_superuser_pair')->where('id', 1)->lockForUpdate()->first(); $row = DB::table('portal_security_approvals')->where('id', $id)->lockForUpdate()->first(); abort_unless($row && $row->kind === 'replacement' && (int) $row->requested_by === $actor->id, 403); abort_unless($row->approved_by !== null && $row->consumed_at === null && strtotime($row->expires_at) > time(), 409); $scope = json_decode($row->scope, true, 512, JSON_THROW_ON_ERROR); $holders = [(int) $pair->first_user_id, (int) $pair->second_user_id]; abort_unless((int) $pair->version === $scope['pair_version'] && in_array((int) $row->approved_by, $holders, true) && in_array($scope['old_user_id'], $holders, true) && ! in_array((int) $row->subject_user_id, $holders, true), 409, 'Dupla ou aprovação mudou.'); $candidate = User::whereKey($row->subject_user_id)->firstOrFail(); abort_unless($candidate->hasVerifiedEmail() && $candidate->two_factor_secret && $candidate->two_factor_confirmed_at, 422); $column = (int) $pair->first_user_id === $scope['old_user_id'] ? 'first_user_id' : 'second_user_id'; // A single UPDATE preserves two distinct holders at every observable state. DB::table('portal_superuser_pair')->where('id', 1)->update([$column => $candidate->id, 'version' => $pair->version + 1, 'approved_by' => $row->approved_by, 'updated_at' => now()]); DB::table('portal_security_approvals')->where('id', $id)->update(['consumed_at' => now(), 'updated_at' => now()]); $this->access->revokeSessions(User::findOrFail($scope['old_user_id'])); $this->access->audit($actor, 'superuser_replaced', ['approval_id' => $id, 'before' => $holders, 'after' => DB::table('portal_superuser_pair')->where('id', 1)->first(), 'alert' => 'Substituição de titular: verificar acessos e recuperação.']); }); } public function requestElevation(User $actor, string $action, ?int $recordId, string $reason): string { $this->administration->administrative($actor); abort_unless(in_array($action, ['view', 'create', 'edit', 'validate', 'publish', 'archive'], true) && mb_strlen($reason) >= 20 && mb_strlen($reason) <= 1000, 422); if ($action === 'view') { $record = $recordId === null ? null : \App\Models\Opportunity::find($recordId); abort_unless($record && $this->access->hasOperationalReadScope($actor, $record) && ! $this->access->has($actor, 'auditor'), 403, 'A consulta exige perfil operacional com âmbito válido; a elevação não o substitui.'); } $id = (string) Str::uuid(); DB::table('portal_security_approvals')->insert(['id' => $id, 'kind' => 'elevation', 'requested_by' => $actor->id, 'subject_user_id' => $actor->id, 'scope' => json_encode(['action' => $action, 'record_id' => $recordId], JSON_THROW_ON_ERROR), 'reason' => $reason, 'expires_at' => now()->addMinutes(15), 'created_at' => now(), 'updated_at' => now()]); $this->access->audit($actor, 'elevation_requested', ['approval_id' => $id, 'action' => $action, 'record_id' => $recordId, 'reason' => $reason]); return $id; } }