6c01d3e08c
Портал давал человеку два обещания про запись его разговора — «живёт месяц и
стирается сама» и «потребуешь удалить свои данные — удалим и голос». Обе чистки
были построены и обе ходили в хранилище с именем 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>
184 lines
10 KiB
PHP
184 lines
10 KiB
PHP
<?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'),
|
||
],
|
||
|
||
];
|