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

- колонка `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
+68 -5
View File
@@ -6,15 +6,17 @@
OIDC, чем предъявляется сессия, что её прекращает и какие адреса остаются
открытыми.
Разграничения записей по владельцу здесь **нет**: всякий вошедший видит ровно
то же, что видел прежде аноним. Его заводит отдельная задача, и до неё сессия
отвечает только на вопрос «узнан ли пришедший», а не «чьё он смотрит».
Здесь же разграничение записей по владельцу: с 2026-08-14 сессия отвечает не
только на вопрос «узнан ли пришедший», но и на «чьё он смотрит». Запись из веба
принадлежит тому, кто её принёс, и чужая неотличима от несуществующей.
Записи, принятые ботом, владельца не имеют вовсе и по API не достаются никому:
связи чата Telegram с учётной записью приложения сервис не ведёт, её заводит
отдельная задача.
Вход из Telegram эта capability не нормирует: бот проверяет отправителя своим
белым списком, и с учётной записью приложения тот список не связан.
## Requirements
### Requirement: Вход через внешнего провайдера
Сервис SHALL заводить сессию только по итогу входа у внешнего провайдера OIDC.
@@ -345,3 +347,64 @@ MUST не делать. Кто допущен, определяет правил
- **WHEN** сервис поднимается с пустым или негодным ключом секции входа
- **THEN** старт кончается отказом, а отказ называет имена ключей
- **AND** значений этих ключей в отказе нет
### Requirement: У записи есть владелец, и чужую ей не отдают
Сервис SHALL заводить у каждой записи, принятой **по HTTP**, — владельца, то
есть учётную запись, от имени которой запись принята, — и MUST отдавать данные
такой записи только её владельцу. Владелец назначается один раз, при приёме, и
MUST не меняться у записи, у которой владелец есть: совместного доступа, ролей и
передачи записи другому сервис не знает. Оговорка не случайна — назначить
владельца записи, у которой его нет, вправе задача, заводящая связь чата
Telegram с учётной записью.
Владелец MUST браться из предъявленной сессии и ниоткуда больше. Владелец,
пришедший полем запроса, дал бы всякому вошедшему право завести запись на чужое
имя.
Обращение к чужой записи MUST быть неотличимо от обращения к несуществующей.
Отдельный отказ «доступ запрещён» превращает опрос в перебор — по разнице
ответов считывается, какие записи заведены, а идентификатор записи и есть то,
что разграничение прячет. Каким именно ответом это выражено, нормирует
capability `intake`: там живёт адрес опроса, и держатель нормы обязан быть один.
Пустой владелец MUST не совпадать ни с одной записью — ни со своей, ни с чужой,
ни с ничьей. Правило записано со стороны **спрашивающего**, а не со стороны
записи: обязательность владельца, которую держит одна лишь подпись метода, пустую
строку пропускает, и первый же вызывающий без учётной записи получил бы ровно
множество записей без владельца, то есть все записи бота.
Записи, принятые из Telegram, владельца не имеют: связи чата с учётной записью
приложения сервис не ведёт. Такая запись MUST не доставаться по API никому —
ответ на неё тот же, что и на несуществующую, — а её расшифровка уезжает
отправителю в чат, как и прежде.
#### Scenario: Своя запись доступна
- **GIVEN** человек вошёл и принял запись
- **WHEN** он спрашивает состояние этой записи своей сессией
- **THEN** ответ несёт состояние записи
#### Scenario: Чужая запись неотличима от несуществующей
- **GIVEN** запись принята одним вошедшим
- **WHEN** её состояние спрашивает другой вошедший
- **THEN** ответ тот же, что и на неизвестный идентификатор, — и кодом, и телом
#### Scenario: Владельца не задают запросом
- **WHEN** запрос на приём записи несёт своё значение владельца
- **THEN** владельцем принятой записи становится предъявитель сессии
#### Scenario: Запись из Telegram не достаётся по API
- **GIVEN** запись принята ботом
- **WHEN** её состояние спрашивает вошедший человек
- **THEN** ответ тот же, что и на неизвестный идентификатор
#### Scenario: Пустой владелец не открывает ничего
- **GIVEN** заведены три задачи: своя, чужая и принятая ботом
- **WHEN** состояние каждой спрашивают с пустым владельцем
- **THEN** ответ на все три тот же, что и на неизвестный идентификатор