- шаг схемы закрывает поверхность, которую хранилище приносит открытой: собственную регистрацию, вход по паролю и одноразовый код — без этого закрытие приёма обходилось двумя запросами - продление сессии выключено, срок семь суток: иначе отзыв доступа у провайдера до сервиса не доходит никогда - файл записи отдаётся вошедшему по токену файла — пересмотр ADR-2026-08-12-file-link-open-but-not-logged
5.5 KiB
Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале
- Дата: 2026-08-12
- Источник: ../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md, раздел «Поле файла не помечаем защищённым, но ссылка не уезжает в журнал»
- Статус: заменено на ADR-2026-08-12-protected-file-behind-session
Решение
Поле файла в хранилище не помечается защищённым: ссылка
/api/files/<коллекция>/<запись>/<имя> работает без токена, и право пройти по
ней даёт знание самой ссылки. Взамен имя файла в хранилище не пишется в журнал
ни в каком виде — ни на успешном пути, ни в тексте отказа.
Почему
Очевидный подход к файлам, отдаваемым в интернет, — закрыть их: у хранилища для этого есть пометка «защищённое поле», и тогда файл отдаётся только по отдельному файловому токену. Мы от неё отказываемся, и цитата из источника называет причину:
Защищённое поле требует отдельного файлового токена. Не помечаем: сегодня право прочитать задачу даёт знание её идентификатора, и файл встаёт вровень с
GET /api/status/:id, а не ниже.
Отказ дешёв ровно до тех пор, пока ссылку неоткуда взять. Перевод хранилища это условие сломал, и вот чем:
Изъятие выписано под путь на диске:
data/files/<uuid>.oggчитателю журнала бесполезен. После перевода имя файла в хранилище — это последняя часть ссылки/api/files/..., по которой запись скачивает кто угодно; строка журнала стала бы бессрочным ключом к чужому аудио.
Путь был построен ревью и прогнан: отказ чтения из хранилища нёс ключ файла
целиком, строка уходила в журнал, а анонимный запрос по собранному адресу
отвечал 200 с телом записи. Второй путь шёл через отказ выгрузки в Object
Storage — тот несёт полный URL объекта.
Отсюда вторая половина решения, без которой первая недопустима: отказы
обрываются. Наружу идёт свой текст с идентификатором записи, а чужая цепочка
%w — нет. В журнал приёма вместо имени идёт расширение собственным полем;
прослеживаемость от этого не страдает.
Запись попадает в журнал как намеренный отказ от очевидного подхода: закрыть файлы токеном предложат снова, и без записанной причины предложение выглядит бесплатным.
Последствия
+ссылка работает без токена, и приёмка проверяется обычным запросом; будущее приложение получает файл без отдельного механизма выдачи токенов.+изъятие из инварианта приватности не расширилось: в журнале по-прежнему только расширение, а не имя.−ссылка, единожды утёкшая, работает бессрочно: отзыва у неё нет, а файлы не удаляются вовсе. Утечка возможна не только журналом — любой будущий экран, показывающий ссылку, наследует это свойство.−появилась норма, которую держит не построение, а внимание: всякий новый отказ хранилища надо обрывать руками. Норму сторожат требование capabilitystorageи проверка журнала, но компилятор — нет.−диагностируемость отказов упала: обрывая цепочку, мы теряем причину. У выгрузки в Object Storage это смягчено — сохраняется класс отказа SDK (AccessDenied,NoSuchBucket), в котором адреса не бывает.- Решение действует до разграничения доступа: задачи
oidc-loginиrecord-ownershipменяют условие, и тогда пометку стоит пересмотреть новой записью.