diff --git a/app/Http/Middleware/EnsureScopedPortalAdministration.php b/app/Http/Middleware/EnsureScopedPortalAdministration.php index 8e90a95a..5f1f34ae 100644 --- a/app/Http/Middleware/EnsureScopedPortalAdministration.php +++ b/app/Http/Middleware/EnsureScopedPortalAdministration.php @@ -19,7 +19,8 @@ final class EnsureScopedPortalAdministration $path = $request->path(); $interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*'); // Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged. - $catalogue = $request->is('management/economic-references/lot-01', 'management/economic-references/lot-01/*', + $catalogue = $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*', + 'management/economic-references/lot-01', 'management/economic-references/lot-01/*', 'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*', 'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*', 'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos', diff --git a/app/Services/TerritoryGeometryImport.php b/app/Services/TerritoryGeometryImport.php index c7e0eb2f..b413d1a9 100644 --- a/app/Services/TerritoryGeometryImport.php +++ b/app/Services/TerritoryGeometryImport.php @@ -20,7 +20,7 @@ class TerritoryGeometryImport private function guard(User $actor, bool $write = false): void { - abort_unless($actor->can('manage-portal'), 403); + abort_unless($actor->can('manage-portal') || app(ScopedPortalAdministration::class)->administrative($actor), 403); abort_unless(config('geometry-import.enabled'), 503, 'Importação cartográfica não disponibilizada.'); if (! $write) { return; diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 05598698..57a5683a 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -2285,3 +2285,8 @@ O staging dos originais, backup/restauro e configuração inicial ainda exigem o Origem das restrições: decisões expressas da Tempo e requisitos anteriores do importador para preservação/backup e autenticação. Enquadramento e mensagens são escolhas de implementação, sem novo papel ou aprovador. Fonte oficial aceite reutilizada sem exigir nova validação da credibilidade. Provas na pasta desta recuperação: inventário dos seis ficheiros, prévias/recibos por nível, `verification.json`, `homologation-readonly.json` e capturas. TypeScript, build QA e sintaxe PHP verificados; provas editoriais anteriores não repetidas. + + +### Ajuste de acesso encontrado na disponibilização — 22/10/2026 + +A conta real de Superutilizador encontrou HTTP403 no importador: o middleware não incluía a cartografia no âmbito administrativo já aprovado e o serviço exigia exclusivamente o gate legado `manage-portal`. Corrigidos ambos para reutilizar `ScopedPortalAdministration`, com MFA de sessão e as exclusões existentes de Auditor. Não foram alteradas atribuições, papéis, contas ou o gate global. Origem do bloqueio: escolha antiga de implementação, não decisão organizacional da Tempo. Três testes transaccionais novos/15 asserções em QA: Superutilizador com MFA chega à validação de fonte/aplicação; sem MFA e conta comum são recusados; nenhum grant legado criado. Provas QA cartográficas aceites reutilizadas sem repetição. diff --git a/tests/Feature/TerritoryGeometryApprovedAdministrationTest.php b/tests/Feature/TerritoryGeometryApprovedAdministrationTest.php new file mode 100644 index 00000000..849c6e94 --- /dev/null +++ b/tests/Feature/TerritoryGeometryApprovedAdministrationTest.php @@ -0,0 +1,61 @@ +holder(); + $this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]); + $this->assertFalse($actor->can('manage-portal')); + // A missing exemplar must produce the source error, not deny the approved actor. + try { + app(TerritoryGeometryImport::class)->preview($actor, 1); + $this->fail('The deliberately absent source must be reported.'); + } catch (HttpException $exception) { + $this->assertSame(422, $exception->getStatusCode()); + } + $this->getJson('/management/territory-geometry-import')->assertStatus(422); + $this->postJson('/management/territory-geometry-import', [])->assertStatus(422); + $this->assertFalse($actor->can('manage-portal')); + } + + public function test_superuser_without_mfa_session_cannot_import(): void + { + $actor = $this->holder(); + $this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden(); + $this->postJson('/management/territory-geometry-import', [])->assertForbidden(); + } + + public function test_ordinary_account_does_not_gain_cartographic_administration(): void + { + $this->holder(); + $actor = User::factory()->create(); + $this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden(); + $this->postJson('/management/territory-geometry-import', [])->assertForbidden(); + } + + private function holder(): User + { + $identity = DB::selectOne('select current_database() as name,inet_server_port() as port'); + $this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name); + $this->assertSame(55441, (int) $identity->port); + config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'geometry-import.enabled' => true, + 'geometry-import.directory' => storage_path('app/private/nonexistent-demarcation-source')]); + $actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()]); + $other = User::factory()->create(); + DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]); + return $actor; + } +}