Files
portal/app/tests/Feature/EndpointAuthHardeningTest.php
T
Дмитрий 2a34ee880a fix(security): закрыть открытые эндпоинты + SSRF-гард webhook перед go-live
- /api/dashboard/summary, /api/managers, /api/lead-statuses: были без auth
  (tenant_id параметром) → auth:sanctum (+tenant); tenant_id из authed-user,
  не из параметра — закрывает кросс-tenant утечку KPI/списка пользователей
- ManagerController: явный where(tenant_id) поверх RLS (BYPASSRLS-роли/тесты)
- WebhookUrlGuard + webhooks/test: SSRF-блок private/reserved/loopback IP
  (cloud-metadata 169.254.169.254 и пр.); update()/delivery — follow-up
- TDD: +EndpointAuthHardeningTest(5) +WebhookSsrfGuardTest(10); обновлены
  Dashboard/Lookups/LeadStatuses тесты под auth
- регрессия tests/Feature 960/964 (2 фейла pre-existing: Vite-manifest env +
  RouteSupplierLeadJobBilling idempotency — оба фейлят и на чистом base)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:15:05 +03:00

59 lines
2.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
/**
* Go-live security: lookup/дашборд эндпоинты до этого были открыты (без
* auth-middleware, tenant_id параметром) — любой неавторизованный мог получить
* KPI/список пользователей произвольного тенанта по ?tenant_id={чужой}.
*
* Закрытие: auth:sanctum + tenant, tenant_id из authed-user (как DealController J1).
*/
// --- 401 без авторизации ---
test('GET /api/dashboard/summary без авторизации возвращает 401', function () {
$this->getJson('/api/dashboard/summary')->assertStatus(401);
});
test('GET /api/managers без авторизации возвращает 401', function () {
$this->getJson('/api/managers')->assertStatus(401);
});
test('GET /api/lead-statuses без авторизации возвращает 401', function () {
$this->getJson('/api/lead-statuses')->assertStatus(401);
});
// --- cross-tenant: tenant_id из user, параметр чужого тенанта игнорируется ---
test('dashboard/summary берёт tenant из authed-user, игнорирует ?tenant_id чужого', function () {
$mine = Tenant::factory()->create(['balance_rub' => '111.00', 'balance_leads' => 11]);
$other = Tenant::factory()->create(['balance_rub' => '999.00', 'balance_leads' => 99]);
$this->actingAs(User::factory()->for($mine)->create());
$this->getJson("/api/dashboard/summary?tenant_id={$other->id}")
->assertOk()
->assertJsonPath('balance.amount_rub', '111.00');
});
test('managers берёт tenant из authed-user, не отдаёт пользователей чужого тенанта', function () {
$mine = Tenant::factory()->create();
$other = Tenant::factory()->create();
$me = User::factory()->for($mine)->create(['first_name' => 'Свой', 'last_name' => 'Менеджер', 'is_active' => true]);
User::factory()->for($other)->create(['first_name' => 'Чужой', 'last_name' => 'Менеджер', 'is_active' => true]);
$this->actingAs($me);
$names = $this->getJson("/api/managers?tenant_id={$other->id}")
->assertOk()
->json('managers.*.name');
expect($names)->toContain('Свой М.');
expect($names)->not->toContain('Чужой М.');
});