у записи появился владелец: чужую больше не отдают

- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы
  `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и
  несуществующая дают один ответ; правило просмотра файлов сужено им же
- приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи
  пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный
  файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается
- удаление учётной записи с записями отвергается стражем, и вешает его сама
  сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
av
2026-08-14 12:18:11 +03:00
parent b7d4660aef
commit 8af8ec2e54
46 changed files with 2520 additions and 101 deletions
+24 -2
View File
@@ -45,6 +45,7 @@ Object Storage — каждая своей записью.
| --- | --- | --- |
| `id` | TEXT PK | Идентификатор записи, выдаёт хранилище |
| `file` | file | Сам файл; пусто у копии в Object Storage |
| `owner` | relation → `users` | Владелец файла; пусто у файлов записи, принятой ботом |
| `location` | select | `local` или `s3` |
| `object_key` | TEXT | Ключ объекта; пусто у местной копии |
| `size` | INTEGER | Размер в байтах |
@@ -60,6 +61,7 @@ capability, и третий смысл развёл бы одно слово п
| Поле | Тип | Что |
| --- | --- | --- |
| `id` | TEXT PK | Идентификатор записи, выдаёт хранилище |
| `owner` | relation → `users` | Владелец записи; пусто у записей, принятых ботом |
| `state` | select | `created`, `converted`, `transcribe`, `done`, `failed`, `dead`; перечень закрыт схемой |
| `source` | select | `api`, `telegram`, `unknown` |
| `file` | relation → `files` | **Текущий** файл задачи: шаг конвейера переставляет ссылку на свой результат |
@@ -83,8 +85,28 @@ capability, и третий смысл развёл бы одно слово п
«мертва»». Схеме принадлежит только закрытость перечня: шестое состояние
потребует нового шага.
**Правила доступа обеих коллекций пусты**, то есть перечислять и читать записи
может только владелец панели. Проверено прогоном: анонимный запрос к
**Владелец записи** заведён шагом `202608140001` — связью с коллекцией `users` в
обеих таблицах. Пустое значение допустимо, и это решение с ценой: записи,
принятые ботом, владельца не имеют вовсе, потому что связи чата Telegram с
учётной записью сервис не ведёт. Обязательность для приёма по HTTP держит
поэтому сам приём, а не схема.
Выборка по владельцу сужает **чтение задачи**: чужая, ничья и несуществующая
дают один и тот же отказ. Выборку воркера владелец не сужает — конвейер
обрабатывает записи всех. Тот же шаг сужает правило просмотра коллекции
`files` владельцем: прежнее правило пускало всякого вошедшего, и знание
идентификатора файловой записи равнялось праву скачать чужое аудио.
**Учётная запись с задачами не удаляется.** Каскадное удаление у связи выключено,
но одного этого мало: при выключенном каскаде хранилище снимает ссылку и
сохраняет запись без проверок — задачи остались бы, но стали бы ничьими, а ничья
задача не достаётся никому. Отказ ставит слой приложения `GuardOwnerDeletion`,
а не правило коллекции: панель ходит правами суперпользователя, и правило её не
судит. Цена названа прямо — владелец панели упирается в отказ, а удаления
записей в сервисе пока нет вовсе.
**Правила доступа задач пусты**, то есть перечислять и читать их может только
владелец панели. Проверено прогоном: анонимный запрос к
`/api/collections/*/records` отвечает `403`, к `/api/logs`, `/api/backups`,
`/api/settings` и `/api/crons``401`.