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

- колонка `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
@@ -0,0 +1,61 @@
## Why
Сегодня знание идентификатора задачи и есть право её читать: всякий вошедший
видит любую расшифровку по её идентификатору — свою, соседа, чью угодно. Паспорт
обещает приглашённому пользователю обратное: «каждый видит только свои записи».
Разграничение стоит первым в очереди не само по себе. На владельце записи стоят
список записей, дедупликация, удаление, учёт расхода и квота — всё, что проект
собирается делать дальше. Заводить их поверх общей кучи записей значит переделать
каждое из них потом.
## What Changes
- У записи появляется **владелец** — учётная запись, от имени которой её
завели. Назначается он один раз, при приёме, и больше не меняется.
- **Опрос готовности отдаёт только свои записи.** Чужая запись по её
идентификатору отвечает «не найдено» — тем же ответом, что и несуществующая.
Отдельного «доступ запрещён» нет намеренно: по нему перебирается список
заведённых задач.
- **Приём по HTTP заводит владельца** из предъявленной сессии. Приём без сессии
отказывал и раньше, и это не меняется.
- **Конвейер владельцем не сужается**: расшифровка идёт для всех записей подряд,
как и прежде. Владелец решает, кому запись показывать, а не кому её считать.
- **BREAKING** для содержимого базы: колонка владельца добавляется шагом схемы, и
записи, заведённые до него, владельца не получают. Данные не переносим —
проект заводится с чистого листа, так решено рамками задачи.
Записи, пришедшие ботом, владельца здесь не получают: связи чата Telegram с
учётной записью приложения ещё нет, её заводит следующая задача. Что это значит
для обязательности колонки — развилка, вынесенная в design.md и на чекпоинт.
## Capabilities
### New Capabilities
Новых нет: разграничение доступа — это поведение уже заведённой `access`, а не
новый домен.
### Modified Capabilities
- `access`: появляется владелец записи и правило «чужая запись неотличима от
несуществующей». Сегодня спека прямо говорит обратное — «разграничения записей
по владельцу здесь нет».
- `intake`: приём назначает владельца принятой записи, опрос готовности сужается
владельцем. Обе строки спеки, обещающие обратное, снимаются.
- `pipeline`: записывается то, что до сих пор было умолчанием, — выборка задачи
воркером владельцем не сужается.
- `storage`: у задачи в схеме появляется колонка владельца, и заводится она без
умолчания.
## Impact
- схема хранилища: новый шаг с колонкой владельца в таблице задач; `docs/database.md`;
- `internal/entity`, `internal/contract`: у задачи появляется владелец, чтение
сужается им;
- `internal/adapter/repo/pocketbase`: четыре места правки колонок очереди —
`applyToRecord`, `recordToJob`, `acquireColumns`, `acquiredRow`;
- `internal/service`: оба метода заведения задачи;
- `internal/controller/http`: опрос готовности и приём;
- публичный контракт `GET /api/status/{id}`: чужая запись начинает отвечать
`404` там, где прежде отдавала содержимое. Форма ответа не меняется.