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

- колонка `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,130 @@
## Критерии приёмки задачи
Дословно из `tasks/items/record-ownership.md`. Файл задачи закрытие удалит —
критерии обязаны его пережить.
- Запрос чужой записи по её идентификатору возвращает «не найдено», а не
содержимое и не «доступ запрещён». Оракул — тест: две сессии, задача первой
запрашивается второй, ответ 404 и пустое тело.
*Расхождение:* «пустое тело» разошлось с нетронутым сценарием спеки, где ответ
по неизвестному идентификатору несёт сообщение о ненайденной задаче. Читается
как «то же тело, что и у неизвестного идентификатора, и без состояния и текста
расшифровки» — иначе реализация по букве критерия молча снимет действующее
требование о сообщении.
- Запись, заведённая из веба, принадлежит вошедшему. Оракул — тест приёма по
HTTP со сверкой колонки владельца.
- Выборка воркера владельцем **не** сужается: конвейер обрабатывает записи всех.
Оракул — тест: задачи двух владельцев проходят конвейер одним воркером.
- База заводится с чистого листа, колонка владельца обязательна и без умолчания.
Оракул — прогон миграций на пустой базе и попытка вставки без владельца.
**Четвёртый критерий разошёлся с рамками той же задачи** и правится решением
человека на чекпоинте: рамки запрещают назначать владельца записям из Telegram, а
обязательная колонка такую запись вставить не даст. Разобрано в `design.md`,
«Open Questions»; ниже план написан по рекомендованному способу — колонка без
умолчания, допускающая пустое значение, обязательность приёма по HTTP держит код.
## Рубрика ревью дизайна — приёмочные критерии
Порождена проходом `rubric` до чтения артефактов. Приёмка судится по одному
списку: этот блок и блок выше.
1. Чужая запись неотличима от несуществующей на всех наблюдаемых осях: тот же
код, то же тело, та же форма ответа.
2. Сужение живёт в одном месте пути чтения и обязательно к употреблению: второго
читающего входа, у которого сужение можно не позвать, нет.
3. Пустой владелец на входе чтения не совпадает ни с одной записью — своей,
чужой и ничьей.
4. Записи без владельца недостижимы сужённым путём никому, и недостижимость
выведена из правила, а не из того, что таких записей мало.
5. Владелец назначается сервером из сессии: поле владельца, пришедшее запросом,
на результат не влияет.
6. Владелец появляется в той же операции, что и запись; отказ по отсутствию
владельца не оставляет ни задачи, ни файла, и способ этого назван.
7. Неизменность владельца держит механизм, а не обещание: назван каждый путь,
которым владельца можно переписать, и что его удерживает.
8. Колонка прочитана всеми четырьмя местами правки колонок очереди плюс шагом
схемы.
9. Воркер владельцем не сужается, и это записано нормой, а не оставлено
умолчанию.
10. Судьба записи при исчезновении владельца определена.
11. Шаг схемы: одно представление «владельца нет», откат не требует переписывания
применённого шага.
12. Проверка владельца и чтение состояния берутся из одного чтения записи, а не
двумя раздельными.
13. Ответ отправителю адресуется по источнику записи, а не по владельцу: запись
без владельца получает свой ответ в чат.
14. Ни ответ, ни журнал не выдают того, что прячет разграничение; различение
чужой и несуществующей в журнале допустимо.
## 1. Схема хранилища
- [x] 1.1 Новый шаг схемы `202608140001_record_owner.go`: колонка `owner` в
таблице задач связью с коллекцией `users`, без умолчания, пустое значение
допустимо, каскадное удаление выключено. Имя колонки — `owner`, поле
сущности — `OwnerID`; имена названы здесь, потому что разойтись им есть где
— семь мест плюс подпись метода
- [x] 1.2 Тем же шагом — колонка `owner` в таблице файлов, теми же свойствами;
правило просмотра коллекции файлов сужается владельцем вместо прежнего
«всякий вошедший»
- [x] 1.3 Тем же шагом — запрет удаления учётной записи, у которой остались
задачи: отказ с причиной, а не снятие ссылки
- [x] 1.4 Прежние шаги схемы не тронуты — проверяется шагом гейта `migrations`
- [x] 1.5 `docs/database.md`: строки колонок в обеих таблицах, правило выборки по
владельцу, новое правило просмотра файлов и запрет удаления учётной записи
## 2. Сущность и контракт
- [x] 2.1 `internal/entity`: у задачи расшифровки появляется владелец
- [x] 2.2 `internal/contract`: читающий метод репозитория задач принимает
владельца; второго читающего метода не заводится
## 3. Хранилище задач
- [x] 3.1 `applyToRecord` кладёт владельца при заведении
- [x] 3.2 `recordToJob` читает владельца
- [x] 3.3 `acquireColumns` и `acquiredRow` читают колонку владельца
- [x] 3.4 `applyOwnedByPipeline` владельца **не** трогает
- [x] 3.5 Чтение задачи сужено владельцем: задача другого владельца и задача без
владельца отдают ту же ошибку, что и несуществующая
## 4. Приём и опрос
- [x] 4.1 `internal/service`: метод заведения задачи из веба принимает владельца
и отказывает при пустом; метод заведения из Telegram владельца не
назначает. Владелец кладётся и на файл, заводимый при приёме
- [x] 4.2 `internal/controller/http`: приём берёт владельца из предъявленной
сессии
- [x] 4.3 `internal/controller/http`: опрос готовности передаёт владельца в
хранилище и отвечает `404` с прежним телом на чужую и на ничью задачу
## 5. Проверки
- [x] 5.1 Тест: две сессии, задача первой запрашивается второй — `404`, тело без
состояния и текста
- [x] 5.2 Тест: приём по HTTP заводит задачу с владельцем-предъявителем
- [x] 5.3 Тест: приём по HTTP без узнанной учётной записи задачи не заводит
- [x] 5.4 Тест: задачи двух владельцев и задача без владельца проходят конвейер
одним воркером
- [x] 5.5 Тест: шаг конвейера, сохраняющий результат, владельца не затирает
- [x] 5.6 Тест: миграции на пустой базе заводят колонку без умолчания
- [x] 5.7 Тест: чтение с пустым владельцем не отдаёт ни своей, ни чужой, ни
ничьей задачи
- [x] 5.8 Тест: сессия без учётной записи пользователя получает `403` на приёме,
файла и задачи не заводится — узнан, но не запись коллекции пользователей
- [x] 5.9 Тест: вошедший просит токен чужого файла — отказ; своего — успех
- [x] 5.10 Тест: удаление учётной записи с задачами отвергается, без задач —
проходит
- [x] 5.11 `task gate` зелёный
## 6. Архивация
- [ ] 6.1 На архивации выправить `## Purpose` спеки `access`: преамбула
переживает слияние дельт дословно и сегодня утверждает, что разграничения
по владельцу нет. Валидатор преамбулу не судит — вспомнить об этом больше
некому
- [ ] 6.2 Сверить `docs/security.md`, `docs/passport.md` и **`docs/architecture.md`**
(строка «Разграничения записей по владельцу здесь нет»): все три обещают
закрытие разграничением именно этой задачей. Адрес в `architecture.md`
назван отдельно — его нашло ревью, и без него обзор архитектуры отправлял
бы следующего читателя чинить уже закрытое