Files
portal/app/config/filesystems.php
T
Дмитрий 6c01d3e08c обзвон З-2.1: заведено хранилище записей разговоров и переезд звука в него
Портал давал человеку два обещания про запись его разговора — «живёт месяц и
стирается сама» и «потребуешь удалить свои данные — удалим и голос». Обе чистки
были построены и обе ходили в хранилище с именем obzvon_zapisi, которого в
настройках НЕ СУЩЕСТВОВАЛО. Врать они не врали — честно отвечали «не смог
удалить файл», — но удалять им было нечего. Оба обещания были бумажными.

Построено:
  • config/filesystems.php — диск obzvon_zapisi. Драйвер переключается одной
    переменной окружения OBZVON_ZAPISI_DRIVER: в день покупки объектного
    хранилища меняется настройка, а не код;
  • ObzvonZapisHranilishche — единственная дверь к звуку. Приём с проверкой,
    открытие ПОТОКОМ со следом в журнале ПДн ДО чтения, ссылка со сроком в час
    там, где хранилище её умеет, и NULL там, где не умеет;
  • PereveztiZapisJob — переезд записи с сервера робота. Не воскрешает стёртый
    голос, не создаёт вторую копию, не оставляет сироты при проигранной гонке;
  • ZvukovoyFayl — одно место, где живёт знание «что такое запись разговора»:
    расширения, подписи форматов, граница пустой записи в 44 байта.

Три замка изоляции, и все три нужны: RLS на строке, явная сверка клиента и
сверка САМОГО ПУТИ — recording_path заполняет чужая машина, и путь вида
7/../8/golos.wav увёл бы в папку другого клиента при законной сверке клиента.

Исправлена ложь в бумаге. В комментарии диска obzvon_materialy было написано,
что вызов url на закрытом диске бросит исключение и публичной ссылки не
появится. Замерено живым запуском: у местного драйвера url не бросает НИКОГДА,
а молча отдаёт /storage/путь. Ссылка ведёт в папку публичного диска, где записи
нет, — вреда нет, но и обещанной защиты нет. Настоящая защита в том, что url не
зовёт никто.

Ловушка круга: проверка З-2.2 утверждала, что хранилища нет, и покраснела бы от
самой постройки. Вычеркнута не была — переписана. Она стерегла живое: пометить
строку стёртой при живом файле значит выкинуть её из указателя под чистку
навсегда. Теперь «файл жив, а убрать не вышло» строится честно — подставлено
хранилище, у которого delete отвечает отказом.

22 новых сторожа, каждый показан красным в пять заходов. Убрана подмена
настроек в заготовке z22DiskZapisey — пять сторожей З-2.2 теперь охраняют и
наличие хранилища тоже.

Прогон обзвона 212 из 212, статанализ 0 ошибок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:02:17 +03:00

184 lines
10 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
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
/*
* Записи, которые клиент отдаёт нам для обучения робота обзвона
* (задача З-2.3). 🔴 Это голоса живых людей, которые об этом не знают и
* согласия нам не давали.
*
* Отдельный диск, а не папка внутри 'local', ровно по трём причинам:
* • 'serve' => false — у 'local' стоит true, то есть Laravel отдаёт его
* содержимое своим маршрутом. Чужим голосам такого маршрута не нужно;
* • ключа 'url' здесь НЕТ намеренно;
* • корень — под storage/app/private, наружу веб-сервером не отдаётся.
*
* 🔴 ИСПРАВЛЕНО 06.08.2026 (круг З-2.1). Здесь стояло «Storage::disk(...)
* ->url() бросит исключение, и публичной ссылки на чужой голос не
* появится даже по ошибке». Это НЕВЕРНО, и проверено живым запуском:
* у местного драйвера `->url()` не бросает никогда, а молча возвращает
* `/storage/7/golos.wav` (Laravel 13, FilesystemAdapter::getLocalUrl).
* Ссылка ведёт в папку ПУБЛИЧНОГО диска, где материала нет, — она не
* играет и ничего не выдаёт, — но обещанной защиты здесь не было.
* Настоящая защита в том, что `->url()` не зовёт никто: единственная
* дверь — MaterialyKlientaService::otkryt со следом в журнале ПДн.
*
* Внутри диска раскладка `<tenant_id>/<uuid>.<расширение>`: имя файла
* неугадываемо, а принадлежность видна прямо в пути.
*
* 🪤 Диск НЕ заменяет защиту по строкам: путь — всего лишь строка. Кто
* вправе открыть материал, решают RLS на obzvon_materialy_klienta и
* App\Services\Obzvon\MaterialyKlientaService, а не файловая система.
*/
'obzvon_materialy' => [
'driver' => 'local',
'root' => storage_path('app/private/obzvon-materialy'),
'serve' => false,
'visibility' => 'private',
'throw' => false,
'report' => false,
],
/*
* Записи НАШИХ разговоров: робот позвонил человеку, разговор записан
* (задача З-2.1). 🔴 Это голос живого человека, и портал дал про него два
* обещания — «живёт месяц» и «потребуешь удалить — удалим».
*
* 🔴 ДО ЭТОЙ СТРОЧКИ ОБА ОБЕЩАНИЯ БЫЛИ БУМАЖНЫМИ. Чистка по сроку (З-2.2)
* и стирание по требованию человека (З-2.5) были построены и обе ходили в
* хранилище с именем `obzvon_zapisi`, которого здесь НЕ БЫЛО. Врать они не
* врали — честно отвечали «не смог удалить файл», — но удалять им было
* нечего. Имя менять нельзя: на него опираются оба и около двадцати мест в
* проверках.
*
* 🔴 ДРАЙВЕР ПЕРЕКЛЮЧАЕТСЯ НАСТРОЙКОЙ, А НЕ ПРАВКОЙ КОДА. В чертеже
* записано, что звук лежит в объектном хранилище с шифрованием: при трёхстах
* разговорах в день это порядка 290 МБ в сутки на клиента и около 50 ГБ за
* полгода. Но объектное хранилище — деньги владельца, и решения он не
* принимал. Поэтому сегодня драйвер local, а в день покупки меняется ОДНА
* переменная окружения OBZVON_ZAPISI_DRIVER=s3 и заполняются ключи ниже.
* Код `App\Services\Obzvon\ObzvonZapisHranilishche` не трогается вовсе — он
* не знает ни одного пути машины и ходит только через Storage.
*
* Закрытость — тремя вещами сразу, как у диска obzvon_materialy:
* • 'serve' => false — Laravel НЕ регистрирует для диска маршрут отдачи
* и НЕ умеет для него ссылок со сроком (замерено:
* providesTemporaryUrls() = false). Ссылка со сроком в час появится
* сама, когда драйвер станет s3: объектное хранилище это умеет;
* • ключа 'url' НЕТ намеренно;
* • корень под storage/app/private, наружу веб-сервером не отдаётся.
*
* 🔴 ЧЕГО ОТСУТСТВИЕ 'url' НЕ ДАЁТ — и это замерено, а не предположено.
* Ждать, что `->url()` БРОСИТ, нельзя: у местного драйвера он не бросает
* никогда, а молча возвращает `/storage/<путь>` (Laravel 13,
* FilesystemAdapter::getLocalUrl). Ссылка при этом ведёт в папку ПУБЛИЧНОГО
* диска, где записи нет, — то есть не играет и ничего не выдаёт, — но
* защитой это не является. Защита в том, что `->url()` не зовёт НИКТО:
* единственная дверь к звуку — ObzvonZapisHranilishche, и она пишет след в
* журнал ПДн.
*
* Внутри раскладка `<tenant_id>/<звонок>-<uuid>.<расширение>`: имя
* неугадываемо, принадлежность видна прямо в пути.
*
* 🪤 Диск НЕ заменяет защиту по строкам: путь — всего лишь строка, и
* заполнять её будет чужая машина (отчёт робота, З-0.6). Кто вправе
* открыть запись, решают RLS на obzvon_calls и ObzvonZapisHranilishche,
* который сверяет и клиента, и сам путь.
*/
'obzvon_zapisi' => [
'driver' => env('OBZVON_ZAPISI_DRIVER', 'local'),
// local
'root' => storage_path('app/private/obzvon-zapisi'),
'serve' => false,
'visibility' => 'private',
// объектное хранилище — заполняется в день покупки, код не меняется
'key' => env('OBZVON_ZAPISI_KEY'),
'secret' => env('OBZVON_ZAPISI_SECRET'),
'region' => env('OBZVON_ZAPISI_REGION', 'ru-central1'),
'bucket' => env('OBZVON_ZAPISI_BUCKET'),
'endpoint' => env('OBZVON_ZAPISI_ENDPOINT'),
'use_path_style_endpoint' => env('OBZVON_ZAPISI_PATH_STYLE', false),
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];