65 lines
3.6 KiB
PHP
65 lines
3.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Http\Middleware;
|
||
|
||
use Closure;
|
||
use Illuminate\Http\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
/**
|
||
* Гейт SaaS-admin зоны (/api/admin/*) — audit-находка J2.
|
||
*
|
||
* СТОПГЭП (2026-05-25): защита боевой админ-зоны (/admin + /api/admin/*)
|
||
* перенесена на уровень nginx — отдельный HTTP Basic Auth с собственным
|
||
* паролем (`/etc/nginx/.htpasswd-admin`, location ^~ /admin и ^~ /api/admin).
|
||
* Поэтому middleware больше не закрывает зону на проде: дверь держит nginx.
|
||
*
|
||
* Ранее (Sprint 3F) здесь был fail-closed 503 вне dev/testing — он закрывал
|
||
* всю админку на проде наглухо, т.к. настоящий saas-admin SSO (Yandex 360)
|
||
* ещё не готов (гейтится Б-1 + DO-4). Замок 503 снят осознанно: оголять
|
||
* /api/admin/* в интернет нельзя, но nginx-пароль её прикрывает.
|
||
*
|
||
* admin_user_id для audit-trail по-прежнему резолвится трейтом
|
||
* ResolvesAdminUserId (стаб super_admin) — это отдельная зона.
|
||
*
|
||
* G7-B: пока активен impersonation (маркер сессии ИЛИ машинный ключ) —
|
||
* вход в saas-admin зону запрещён (запрет эскалации к другим тенантам).
|
||
*
|
||
* M-1 (приёмка 21.06): nginx-дверь дополнена app-слойным fail-closed гейтом по
|
||
* REMOTE_USER + config-allowlist. Закрывает обходы front-controller'а
|
||
* (/index.php/api/admin, /API/admin), где nginx basic-auth не применяется и
|
||
* REMOTE_USER пуст. См. config/admin.php и spec 2026-06-21-m1-admin-gate-fail-closed.
|
||
*
|
||
* TODO (после Б-1 + DO-4): заменить nginx-дверь на настоящий saas-admin
|
||
* guard (Yandex 360 SSO-сессия + роль).
|
||
*/
|
||
class EnsureSaasAdmin
|
||
{
|
||
public function handle(Request $request, Closure $next): Response
|
||
{
|
||
// G7-B: пока активен impersonation (маркер сессии ИЛИ машинный ключ) —
|
||
// вход в saas-admin зону запрещён (запрет эскалации к другим тенантам).
|
||
$hasMarker = $request->hasSession() && $request->session()->has('impersonation');
|
||
$hasBearer = str_starts_with((string) $request->header('Authorization', ''), 'Bearer lpimp_');
|
||
if ($hasMarker || $hasBearer) {
|
||
abort(403, 'Во время сессии impersonation доступ в админ-зону запрещён.');
|
||
}
|
||
|
||
// M-1 (приёмка 21.06): fail-closed гейт. REMOTE_USER непуст только у запросов,
|
||
// прошедших nginx admin-basic-auth (^~ /admin, ^~ /api/admin); обходы через
|
||
// front-controller (/index.php/api/admin, /API/admin) попадают в auth_basic off
|
||
// → REMOTE_USER пуст → 403. В local/testing гейт выключен (см. config/admin.php).
|
||
if (config('admin.basic_auth_gate')) {
|
||
$remoteUser = (string) $request->server('REMOTE_USER', '');
|
||
$allowlist = (array) config('admin.basic_auth_allowlist', []);
|
||
if ($remoteUser === '' || ! in_array($remoteUser, $allowlist, true)) {
|
||
abort(403, 'Доступ в админ-зону запрещён.');
|
||
}
|
||
}
|
||
|
||
return $next($request);
|
||
}
|
||
}
|