у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
Сознательно не описаны: перенос прежних данных — его нет по решению задачи
|
||||
`pocketbase-storage`; удаление записей и файлов — сервис объявлен архивом
|
||||
2026-08-11, а удаление приносит задача `delete-record`.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Сервис поднимается на чистом каталоге данных
|
||||
|
||||
@@ -106,9 +105,14 @@ MUST завести свою схему и принимать записи об
|
||||
|
||||
Одной пометки мало: защищённый файл судится **коротким токеном файла**, который
|
||||
узнанный отправитель берёт у хранилища, предъявив сессию, — и правилом просмотра
|
||||
коллекции. Правило MUST пускать всякого узнанного: незаданное означает «только
|
||||
владелец панели», и тогда файла не получит и вошедший. Сужения по владельцу
|
||||
здесь нет — его заводит отдельная задача.
|
||||
коллекции. Правило MUST пускать только владельца файла: незаданное означает
|
||||
«только владелец панели», и тогда файла не получит и вошедший, а прежнее «всякий
|
||||
узнанный» отдавало чужое аудио тому, кто знает идентификатор записи.
|
||||
|
||||
Токен файла хранилище выдаёт **на предъявителя**, а не на файл, и о файле при
|
||||
выдаче не спрашивает. Значит владельца судит переход по ссылке, а не выдача
|
||||
токена: отказ наступает там, и требовать его от выдачи значит требовать
|
||||
механизма, которого нет.
|
||||
|
||||
Отсюда порядок для потребителя: сессия → токен файла → ссылка с этим токеном.
|
||||
Браузер с одной лишь кукой файла не получит, и это свойство хранилища, а не
|
||||
@@ -272,3 +276,118 @@ MUST завести свою схему и принимать записи об
|
||||
- **WHEN** сервис запускается снова
|
||||
- **THEN** приглашения завести владельца в журнале нет
|
||||
|
||||
### Requirement: Владелец задачи лежит связью с учётной записью
|
||||
|
||||
Хранилище SHALL держать владельца задачи расшифровки отдельной колонкой — связью
|
||||
с учётной записью, — и эта колонка MUST не иметь умолчания: запись, чей владелец
|
||||
не назван, не достаётся никому по недосмотру схемы.
|
||||
|
||||
Колонка MUST допускать пустое значение, и это решение с названной ценой: записи,
|
||||
принятые ботом, владельца не имеют, потому что связи чата Telegram с учётной
|
||||
записью сервис не ведёт. Обязательность для приёма по HTTP держит сама
|
||||
capability `intake`, а не схема.
|
||||
|
||||
Колонка приезжает **новым шагом схемы**: применённый шаг не переписывается.
|
||||
Записей, заведённых до этого шага, сервис не переносит — проект заводится с
|
||||
чистого листа.
|
||||
|
||||
#### Scenario: Колонка появляется на пустой базе
|
||||
|
||||
- **WHEN** сервис поднимается на чистом каталоге данных
|
||||
- **THEN** у таблицы задач есть колонка владельца
|
||||
- **AND** умолчания у неё нет
|
||||
|
||||
### Requirement: Файл записи сужается владельцем наравне с задачей
|
||||
|
||||
Хранилище SHALL держать владельца и у файла записи — той же связью с учётной
|
||||
записью, тем же шагом схемы, — и правило просмотра файлов MUST пускать к файлу
|
||||
только его владельца. Прежнее правило пускало всякого узнанного, и знание
|
||||
идентификатора файловой записи равнялось праву скачать чужое аудио.
|
||||
|
||||
Без этого требования разграничение закрывает метаданные задачи и оставляет
|
||||
открытым содержимое — то самое, что оно и заведено прятать. Хуже самой дыры была
|
||||
бы отметка о закрытии: паспорт и модель угроз называют исполнителем этой работы
|
||||
именно эту задачу, и слово «закрыто» скрыло бы открытый путь.
|
||||
|
||||
Владелец файла MUST назначаться там же, где владелец задачи, — при приёме, из
|
||||
предъявленной сессии, — и MUST оставаться пустым у файлов, заведённых конвейером
|
||||
для записи без владельца.
|
||||
|
||||
Ссылка на файл в задаче переставляется каждым шагом конвейера, поэтому владелец
|
||||
файла MUST лежать своей колонкой, а не выводиться через задачу: исходная копия
|
||||
после конвертации не связана с задачей ничем.
|
||||
|
||||
Отказ наступает **на переходе по ссылке**, а не на выдаче токена файла: токен
|
||||
хранилище выдаёт на предъявителя, а не на файл, и о файле при выдаче не
|
||||
спрашивает вовсе. Требовать отказа при выдаче значит требовать механизма,
|
||||
которого нет, — а проверка, написанная под такое требование, зеленела бы, не
|
||||
касаясь пути, по которому аудио и уходит.
|
||||
|
||||
#### Scenario: Чужой файл не отдаётся
|
||||
|
||||
- **GIVEN** запись принята одним вошедшим
|
||||
- **WHEN** другой вошедший идёт по ссылке на файл этой записи со своим токеном
|
||||
- **THEN** содержимого он не получает
|
||||
|
||||
#### Scenario: Свой файл отдаётся
|
||||
|
||||
- **GIVEN** человек принял запись
|
||||
- **WHEN** он идёт по ссылке на файл своей записи со своим токеном
|
||||
- **THEN** содержимое отдаётся
|
||||
|
||||
#### Scenario: Файл записи из Telegram не отдаётся по API
|
||||
|
||||
- **GIVEN** запись принята ботом, и владельца у неё нет
|
||||
- **WHEN** вошедший человек идёт по ссылке на её файл со своим токеном
|
||||
- **THEN** содержимого он не получает
|
||||
|
||||
### Requirement: Учётная запись с записями не удаляется
|
||||
|
||||
Хранилище SHALL отвергать удаление учётной записи, у которой остались задачи
|
||||
расшифровки **либо файлы**. Отказ MUST называть причину, и MUST доезжать до
|
||||
спрашивающего: хранилище пропускает наружу только свою ошибку роутера, а всякую
|
||||
другую подменяет сообщением про обязательную связь — подсказкой, по которой
|
||||
владелец панели пойдёт удалять записи руками.
|
||||
|
||||
Считаются обе коллекции с владельцем. Файл переживает свою задачу: шаг конвейера
|
||||
заводит его до сохранения задачи, и потерянный захват оставляет файл с владельцем
|
||||
и без ссылки.
|
||||
|
||||
Запрет MUST ставить сама сборка хранилища, а не вызывающий: сборка, забывшая его
|
||||
позвать, теряет защиту молча — и теряла, пока запрет вешался отдельной строкой
|
||||
запуска, а окружение проверок его не ставило вовсе.
|
||||
|
||||
Удаление при этом не только панельное: умолчание библиотеки разрешает вошедшему
|
||||
удалить **свою** учётную запись запросом, так что запрет закрывает и публичную
|
||||
поверхность.
|
||||
|
||||
Требование заведено вместо прежнего «удаление не уносит задачи следом»: оно
|
||||
выглядело выполненным, а на деле хранилище при выключенном каскаде **снимает
|
||||
ссылку** — задачи остаются, но становятся ничьими, а ничья задача не достаётся
|
||||
по API никому. Архив человека исчезал бы молча и восстановлению не подлежал:
|
||||
прежнего владельца не остаётся нигде.
|
||||
|
||||
Цена требования названа прямо: владелец панели упирается в отказ, а способа
|
||||
удалить записи в сервисе пока нет вовсе — его приносит задача про удаление
|
||||
записи. До неё удаление учётной записи с записями невозможно, и это осознанный
|
||||
тупик, а не недосмотр.
|
||||
|
||||
#### Scenario: Удаление учётной записи с записями отвергается
|
||||
|
||||
- **GIVEN** у учётной записи есть задачи расшифровки
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а отказ называет причину
|
||||
- **AND** задачи и их владелец остаются прежними
|
||||
|
||||
#### Scenario: Учётная запись с одними файлами тоже не удаляется
|
||||
|
||||
- **GIVEN** у учётной записи остались файлы, но задач нет
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а владелец файлов остаётся прежним
|
||||
|
||||
#### Scenario: Учётная запись без записей удаляется
|
||||
|
||||
- **GIVEN** у учётной записи нет ни задач, ни файлов
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление проходит
|
||||
|
||||
|
||||
Reference in New Issue
Block a user