- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
557 lines
42 KiB
Markdown
557 lines
42 KiB
Markdown
# access Specification
|
|
|
|
## Purpose
|
|
|
|
Кто пришёл в сервис и пускают ли его дальше: узнавание по заголовку доверенного
|
|
источника, заведение учётной записи первым обращением и то, какие адреса
|
|
остаются открытыми неузнанному.
|
|
|
|
Своего входа у сервиса нет. Собственный протокол OIDC — с адресом, уводящим к
|
|
провайдеру, возвратом, кукой сессии и её сроком — жил здесь с 2026-08-12 по
|
|
2026-08-22 и убран задачей `trusted-header-login`: пришедшего называет обратный
|
|
прокси, сходивший к провайдеру, и делает это на каждом запросе.
|
|
|
|
Здесь же разграничение записей по владельцу: с 2026-08-14 узнавание отвечает не
|
|
только на вопрос «узнан ли пришедший», но и на «чьё он смотрит». Запись из веба
|
|
принадлежит тому, кто её принёс, и чужая неотличима от несуществующей.
|
|
|
|
Записи без владельца у сервиса не бывает: колонка владельца пустого значения не
|
|
принимает, и норму эту держит capability `storage`. Прежде такие записи заводил
|
|
вход Telegram — связи чата с учётной записью сервис не вёл, — и 2026-08-14 вход
|
|
убран вместе с этим исключением.
|
|
## Requirements
|
|
|
|
### Requirement: Значение, дающее доступ, не печатается
|
|
|
|
Сервис SHALL не писать в журнал, в ответ и в метку метрики ни значение заголовка,
|
|
которым назван пришедший, ни адрес почты пользователя.
|
|
|
|
Записанное значение MUST читаться как ключ к чужому доступу: заголовок целиком
|
|
задаёт тот, кто шлёт запрос, и строка журнала уезжает в собранные логи, откуда её
|
|
не убрать. Требование того же рода, что и запрет писать имя файла на диске: там
|
|
строка журнала собирала бы путь к чужой записи, здесь — имя, которым довольно
|
|
назваться, чтобы стать этим человеком.
|
|
|
|
Короткий токен файла из перечня ушёл вместе с самим токеном: значений на
|
|
предъявителя сервис больше не выдаёт, и запрет остался бы правилом без предмета.
|
|
|
|
Адрес почты приходит от провайдера и принадлежит человеку, а не сервису. Имя из
|
|
заголовка — тоже: это логин человека у провайдера.
|
|
|
|
Идентификатор учётной записи в журнал писать можно и нужно: он выдан сервисом,
|
|
доступа сам по себе не даёт и без него путь запроса не прослеживается.
|
|
|
|
#### Scenario: Значения заголовка нет в журнале
|
|
|
|
- **WHEN** запрос с заголовком проходит через сервис
|
|
- **THEN** значение заголовка не встречается ни в одной журнальной записи
|
|
|
|
#### Scenario: Адреса почты нет в журнале
|
|
|
|
- **WHEN** приходит первое обращение и учётная запись заводится
|
|
- **THEN** адрес почты не встречается ни в одной журнальной записи
|
|
|
|
### Requirement: Кого пускать, решает провайдер
|
|
|
|
Сервис SHALL пускать всякого, кого назвал доверенный источник, и своей проверки
|
|
допуска MUST не делать. Кто допущен, определяет правило провайдера на домен
|
|
сервиса — настройка выкладки, лежащая вне репозитория.
|
|
|
|
Требование записано именно как решение с ценой, а не как умолчание: провайдер
|
|
общий для контура, и правило, настроенное слишком широко, открывает сервис
|
|
всякому, у кого есть учётная запись у провайдера. Проверить это по коду нельзя,
|
|
поэтому граница названа здесь и повторена в модели угроз.
|
|
|
|
Цена сдвинулась в нашу пользу: провайдер судит **каждый** запрос, а не только
|
|
первый. Прежде сервис спрашивал провайдера однажды и потом верил выданному
|
|
значению до его истечения — отзыв доступа доходил до сервиса с задержкой в срок
|
|
жизни этого значения. Теперь отзыв действует со следующего запроса.
|
|
|
|
Обратная сторона у этого одна, и она названа прямо: **весь барьер держится на
|
|
том, что прокси ставит заголовок сам, а не пропускает пришедший**. Прокси,
|
|
пропускающий чужой заголовок, открывает сервис всякому под любым именем.
|
|
Требование к контуру записано в модели угроз; репозиторием оно не проверяется.
|
|
|
|
#### Scenario: Названный провайдером получает доступ
|
|
|
|
- **WHEN** запрос приходит с доверенного адреса с заголовком, поставленным
|
|
прокси
|
|
- **THEN** доступ открывается, а учётная запись заводится, если её не было
|
|
- **AND** сервис не спрашивает у заголовков ничего сверх имени, имени для показа
|
|
и адреса почты
|
|
|
|
#### Scenario: Отзыв у провайдера действует со следующего запроса
|
|
|
|
- **GIVEN** человек работал в сервисе, и провайдер закрыл ему доступ
|
|
- **WHEN** приходит следующий его запрос
|
|
- **THEN** прокси заголовка не ставит, и запрос получает отказ
|
|
|
|
### Requirement: Проба здоровья и метрики остаются открытыми
|
|
|
|
Сервис SHALL отдавать `GET /health` и `GET /metrics` неузнанному. Ни у пробы
|
|
здоровья, ни у сборщика метрик учётной записи нет, и требование узнавания
|
|
остановило бы наблюдение за сервисом.
|
|
|
|
Наружу эти адреса закрывает правило обратного прокси — это работа выкладки, и
|
|
сервис на неё не полагается: содержимого записей и текстов расшифровок оба
|
|
адреса не несут.
|
|
|
|
Заголовок, пришедший с недоверенного адреса, MUST не менять их ответа: узнавание
|
|
отказа не выдаёт, и посторонняя строка в запросе не вправе гасить наблюдение.
|
|
|
|
#### Scenario: Проба здоровья доступна неузнанному
|
|
|
|
- **WHEN** запрос приходит на `GET /health` без заголовка
|
|
- **THEN** ответ имеет код `200`
|
|
|
|
#### Scenario: Метрики доступны неузнанному
|
|
|
|
- **WHEN** запрос приходит на `GET /metrics` без заголовка
|
|
- **THEN** ответ имеет код `200`
|
|
|
|
#### Scenario: Чужой заголовок наблюдения не гасит
|
|
|
|
- **WHEN** запрос на `GET /health` приходит с недоверенного адреса с заголовком
|
|
`Remote-User`
|
|
- **THEN** ответ имеет код `200`
|
|
|
|
### Requirement: У записи есть владелец, и чужую ей не отдают
|
|
|
|
Сервис SHALL заводить у каждой принятой записи владельца — учётную запись, от
|
|
имени которой запись принята, — и MUST отдавать данные такой записи только её
|
|
владельцу. Запись без владельца MUST не заводиться ничем — ни приёмом, ни
|
|
конвейером, ни запросом к базе: колонка владельца пустого значения не принимает,
|
|
и норму эту держит capability `storage`.
|
|
|
|
Владелец назначается один раз, при приёме, и MUST не меняться: совместного
|
|
доступа, ролей и передачи записи другому сервис не знает.
|
|
|
|
Владелец MUST браться из узнанного предъявителя и ниоткуда больше. Владелец,
|
|
пришедший полем запроса, дал бы всякому узнанному право завести запись на чужое
|
|
имя.
|
|
|
|
Обращение к чужой записи MUST быть неотличимо от обращения к несуществующей — и
|
|
к её карточке, и к её тексту, и к её файлу. Отдельный отказ «доступ запрещён»
|
|
превращает чтение в перебор: по разнице ответов считывается, какие записи
|
|
заведены, а идентификатор записи и есть то, что разграничение прячет. Каким
|
|
именно ответом это выражено, нормирует capability `archive`: там живут адреса
|
|
чтения записи, и держатель нормы обязан быть один.
|
|
|
|
Пустой владелец MUST не совпадать ни с одной записью. Правило записано со стороны
|
|
**спрашивающего** и остаётся в силе, хотя записей без владельца в базе не бывает:
|
|
спрашивающий с пустым владельцем — это вызов, у которого нет учётной записи, и
|
|
отвечать ему надо отказом, а не выборкой. Держится оно отдельно от схемы
|
|
намеренно: схема запрещает **заводить** ничью запись, а это правило запрещает
|
|
**спрашивать** ничьим именем, и одно другое не заменяет.
|
|
|
|
#### Scenario: Своя запись доступна
|
|
|
|
- **GIVEN** человек узнан и принял запись
|
|
- **WHEN** он спрашивает карточку этой записи
|
|
- **THEN** ответ несёт данные записи
|
|
|
|
#### Scenario: Чужая запись неотличима от несуществующей
|
|
|
|
- **GIVEN** запись принята одним узнанным
|
|
- **WHEN** её карточку спрашивает другой узнанный
|
|
- **THEN** ответ тот же, что и на неизвестный идентификатор, — и кодом, и телом
|
|
|
|
#### Scenario: Чужой файл неотличим от несуществующего
|
|
|
|
- **GIVEN** запись принята одним узнанным
|
|
- **WHEN** её файл просит другой узнанный
|
|
- **THEN** ответ тот же, что и на неизвестный идентификатор, — и кодом, и телом
|
|
|
|
#### Scenario: Владельца не задают запросом
|
|
|
|
- **WHEN** запрос на приём записи несёт своё значение владельца
|
|
- **THEN** владельцем принятой записи становится узнанный предъявитель
|
|
|
|
#### Scenario: Ничью запись завести нечем
|
|
|
|
- **WHEN** запись пытаются завести с пустым владельцем
|
|
- **THEN** база её не сохраняет
|
|
|
|
#### Scenario: Пустой владелец не открывает ничего
|
|
|
|
- **GIVEN** заведены две записи: своя и чужая
|
|
- **WHEN** карточку каждой спрашивают с пустым владельцем
|
|
- **THEN** ответ на обе тот же, что и на неизвестный идентификатор
|
|
|
|
### Requirement: Приложение узнаёт вошедшего
|
|
|
|
Сервис SHALL отдавать приложению сведения о том, кто пришёл, — `GET /app/me` — и
|
|
MUST отвечать отказом `401`, когда пришедший не узнан. Своей страницы со
|
|
скриптом, которой сервер отрисовал бы имя пришедшего, у сервиса нет: приложение
|
|
собирает разметку само и пришедшего узнаёт ответом.
|
|
|
|
Заголовок ставит прокси, и прочитать его из браузера приложение не может вовсе —
|
|
этот адрес единственный способ узнать, кто пришёл.
|
|
|
|
Ответ MUST нести идентификатор учётной записи и имя, пригодное к показу, полями
|
|
`id` и `name`. Адрес почты MUST в ответ не попадать: он приходит от провайдера и
|
|
принадлежит человеку, а не сервису, и правило о непечатаемых значениях запрещает
|
|
ему выходить наружу наравне с журналом.
|
|
|
|
#### Scenario: Пришедший узнан
|
|
|
|
- **GIVEN** запрос идёт с доверенного адреса с заголовком `Remote-User`
|
|
- **WHEN** приложение спрашивает, кто пришёл
|
|
- **THEN** ответ несёт идентификатор его учётной записи
|
|
|
|
#### Scenario: Пришедший не узнан
|
|
|
|
- **WHEN** приложение спрашивает, кто пришёл, без заголовка
|
|
- **THEN** ответ имеет код `401`
|
|
- **AND** тело ответа не несёт учётной записи
|
|
|
|
#### Scenario: Адреса почты в ответе нет
|
|
|
|
- **GIVEN** пришедший узнан, и у его учётной записи есть адрес почты
|
|
- **WHEN** приложение спрашивает, кто пришёл
|
|
- **THEN** адреса почты в ответе нет
|
|
|
|
### Requirement: Приложение отдаётся без сессии
|
|
|
|
Сервис SHALL отдавать разметку приложения и её ресурсы неузнанному. Перечень
|
|
адресов, открытых без узнавания, пополняется ими: прежде в нём стояли только
|
|
проба здоровья и метрики.
|
|
|
|
Причина внешняя: заголовок ставит прокси, и человек, которого прокси не назвал,
|
|
до приложения не доходит вовсе. Разметка при этом обязана отдаваться и ему —
|
|
иначе неудача узнавания выглядела бы поломкой сервиса, а не отказом входа. Цена
|
|
открытости названа здесь же и невелика: ни разметка, ни ресурсы содержимого
|
|
записей не несут — они одинаковы для всех и собираются до всякого запроса.
|
|
|
|
Открытость MUST не касаться данных: всякий адрес под корнем приложения
|
|
по-прежнему требует узнанного предъявителя, и приложение, открытое неузнанным,
|
|
не получает ни одной записи.
|
|
|
|
#### Scenario: Разметка доступна неузнанному
|
|
|
|
- **WHEN** запрос приходит на корень сервиса без заголовка
|
|
- **THEN** ответ имеет код `200`
|
|
- **AND** тело ответа — разметка приложения
|
|
|
|
#### Scenario: Ресурс приложения доступен неузнанному
|
|
|
|
- **WHEN** запрос приходит на ресурс приложения без заголовка
|
|
- **THEN** ответ имеет код `200`
|
|
|
|
#### Scenario: Данные неузнанному не отдаются
|
|
|
|
- **GIVEN** приложение открыто без заголовка
|
|
- **WHEN** оно спрашивает список записей
|
|
- **THEN** ответ имеет код `401`
|
|
|
|
### Requirement: Пришедшего называет доверенный источник
|
|
|
|
Сервис SHALL узнавать пришедшего по заголовку `Remote-User`, который ставит
|
|
обратный прокси, сходивший к провайдеру, и MUST не вести собственного входа: ни
|
|
адреса, уводящего к провайдеру, ни адреса возврата, ни куки, ни выхода у сервиса
|
|
не остаётся.
|
|
|
|
Заголовку сервис MUST верить только тогда, когда запрос пришёл с адреса из
|
|
объявленного перечня доверенных, и адрес этот MUST браться у самого соединения,
|
|
а не из пересылаемого заголовка: значением пересылаемого распоряжается тот, кто
|
|
шлёт запрос, и барьер, подделываемый той же строкой, которой он обходится, не
|
|
барьер вовсе.
|
|
|
|
Заголовок, пришедший с недоверенного адреса, MUST не узнавать никого. Отказа при
|
|
этом MUST не наступать в самом узнавании: проба здоровья, метрики и разметка
|
|
приложения открыты неузнанному, и отказ на них закрыл бы наблюдение за сервисом
|
|
всякому, кто пришлёт заголовок. Отказ приходит там, где приходил и раньше, —
|
|
требованием учётной записи на адресах приложения.
|
|
|
|
**Узнавание идёт после ограничителя частоты, а не до него.** Оно читает базу, а
|
|
на новом имени ещё и пишет в неё; выполненное раньше ограничителя, оно работало
|
|
бы на запросах, которые тот уже отверг, и поток отвергнутых обращений заводил бы
|
|
учётные записи, которые потом не убираются ничем.
|
|
|
|
**Узнавание действует на объявленной области, а не на всей поверхности сервиса.**
|
|
Область — корень приложения; она MUST выводиться из объявленного адресного
|
|
пространства сервиса, а не перечисляться вторым списком. Прежде область была
|
|
шире на один адрес — тот, которым хранилище выдавало короткий токен файла; ни
|
|
адреса, ни токена не осталось. Прежде область была и уже: собственную поверхность
|
|
хранилища требовалось из неё вычитать, потому что ключ учётной записи лежал в
|
|
коллекции обычной колонкой, а правило правки было библиотечным. Поверхности этой
|
|
нет, и вычитать больше нечего.
|
|
|
|
Сужение области закрывает вещь, которая от смены хранилища не зависит: узнавание
|
|
MUST не срабатывать на пробе здоровья, на метриках и на ресурсах приложения.
|
|
Иначе запрос за каждой картинкой стоил бы обращения к базе, а первый такой запрос
|
|
с новым именем — записи в неё.
|
|
|
|
**Значение заголовка принимается, а не берётся как есть.** Пустое значение и
|
|
значение из одних пробельных знаков MUST не узнавать никого и MUST не заводить
|
|
учётной записи: прокси штатно шлёт пустой заголовок там, где никого не назвал, и
|
|
без этой нормы все неназванные собрались бы в одну учётную запись с общим
|
|
архивом. Запрос, несущий **более одного** значения `Remote-User`, MUST не
|
|
узнавать никого: прокси, настроенный добавлять заголовок вместо замены, оставляет
|
|
рядом со своим значением присланное анонимом, и выбор «первое попавшееся» отдал
|
|
бы вход анониму. Значение сверх объявленного предела длины и значение с
|
|
управляющими знаками MUST не узнавать никого. Сравнение при поиске MUST быть
|
|
точным, знак в знак: приведение регистра склеило бы двух разных людей по правилу,
|
|
которого у провайдера нет. Обрамляющие пробелы при этом MUST срезаться до
|
|
сравнения: они не часть имени, и заголовок с ведущим пробелом называет того же
|
|
человека. Предел длины MUST считаться в **знаках** — той же единицей, что
|
|
считает колонка.
|
|
|
|
Отказ базы при узнавании MUST кончаться отказом сервиса, а не молчаливым
|
|
проходом неузнанным: иначе человек увидит отказ входа там, где легла база.
|
|
|
|
Исход узнавания MUST оставлять строку журнала — и когда заголовок пришёл с
|
|
недоверенного адреса, и когда заголовок пришёл **более чем одним значением**, и
|
|
когда учётная запись заведена. Уровень первых двух MUST быть виден при боевой
|
|
настройке журнала: обе строки означают поломку контура, а поломка, записанная
|
|
уровнем, который в бою выключен, не записана вовсе. Без неё владелец, у
|
|
которого никто не может войти, не отличит своей поломки (перечень доверенных
|
|
адресов) от поломки контура (прокси заголовка не ставит), а это разные поломки в
|
|
разных местах. Строка несёт адрес пира и идентификатор учётной записи и MUST не
|
|
нести значения заголовка.
|
|
|
|
Имя, пригодное к показу, сервис SHALL брать из заголовка `Remote-Name`, адрес
|
|
почты — из `Remote-Email`. Имена всех трёх заголовков нормативны: смена имени
|
|
молча перестаёт узнавать всех, а проверка, которая сама ставит и сама читает своё
|
|
имя, этого не замечает. Контур уже пишет эти имена соседним сервисам.
|
|
|
|
Узнавание MUST идти на каждом запросе, и значения, переживающего запрос, сервис
|
|
MUST не выдавать вовсе — ни куки, ни токена сессии, ни короткого токена файла.
|
|
Исключений у этого правила больше нет: файл записи отдаётся тому же узнаванию,
|
|
что и всё прочее, и отзыв доступа доходит до него сразу.
|
|
|
|
Смысл именно таков: отзыв доступа судит провайдер на каждом обращении, а не
|
|
однажды выданный срок.
|
|
|
|
Собственных токенов сервис не принимает: значения, предъявленного запросом и
|
|
дающего доступ помимо заголовка, у него не существует. Прежде такое значение
|
|
било заголовок — им пользовался владелец панели; панели нет, и правило приоритета
|
|
осталось бы правилом без предмета.
|
|
|
|
Значение заголовка MUST не попадать ни в журнал, ни в ответ, ни в метку метрики.
|
|
Оно приходит строкой запроса и целиком задаётся тем, кто её шлёт, а с
|
|
недоверенного адреса — анонимом; сверх того имя принадлежит человеку наравне с
|
|
адресом его почты.
|
|
|
|
#### Scenario: Заголовок с доверенного адреса узнаёт человека
|
|
|
|
- **GIVEN** адрес источника стоит в перечне доверенных
|
|
- **WHEN** запрос к адресу приложения приходит с заголовком `Remote-User`
|
|
- **THEN** запрос идёт от имени учётной записи с этим значением
|
|
|
|
#### Scenario: Заголовок с недоверенного адреса не узнаёт никого
|
|
|
|
- **GIVEN** адреса источника в перечне доверенных нет
|
|
- **WHEN** запрос к адресу приложения приходит с тем же заголовком
|
|
- **THEN** ответ имеет код `401`
|
|
- **AND** учётной записи с этим значением не появляется
|
|
|
|
#### Scenario: Предъявленного значения сервис не признаёт
|
|
|
|
- **GIVEN** запрос несёт заголовок `Remote-User` и постороннее значение доступа
|
|
в заголовке или в параметре
|
|
- **WHEN** сервис решает, кто пришёл
|
|
- **THEN** пришедшим считается названный заголовком
|
|
|
|
#### Scenario: Пустой заголовок не узнаёт никого
|
|
|
|
- **GIVEN** адрес источника стоит в перечне доверенных
|
|
- **WHEN** запрос к адресу приложения приходит с пустым `Remote-User`
|
|
- **THEN** ответ имеет код `401`
|
|
- **AND** учётной записи не появляется
|
|
|
|
#### Scenario: Два значения одного заголовка не узнают никого
|
|
|
|
- **GIVEN** адрес источника стоит в перечне доверенных
|
|
- **WHEN** запрос к адресу приложения несёт два значения `Remote-User`
|
|
- **THEN** ответ имеет код `401`
|
|
- **AND** учётной записи не появляется
|
|
|
|
#### Scenario: Значение сверх предела длины не узнаёт никого
|
|
|
|
- **GIVEN** адрес источника стоит в перечне доверенных
|
|
- **WHEN** запрос несёт `Remote-User` длиннее объявленного предела
|
|
- **THEN** ответ имеет код `401`
|
|
- **AND** учётной записи не появляется
|
|
|
|
#### Scenario: Область узнавания — корень приложения
|
|
|
|
- **GIVEN** сервис поднялся
|
|
- **WHEN** смотрят, на каких адресах срабатывает узнавание
|
|
- **THEN** это адреса под корнем приложения, и второго списка адресов нет
|
|
|
|
#### Scenario: Проба здоровья учётной записи не заводит
|
|
|
|
- **GIVEN** учётной записи с этим значением ещё нет
|
|
- **WHEN** запрос с заголовком приходит на `GET /health` с доверенного адреса
|
|
- **THEN** учётной записи не появляется
|
|
|
|
#### Scenario: Недоверенный источник виден в журнале
|
|
|
|
- **WHEN** запрос с заголовком приходит с недоверенного адреса
|
|
- **THEN** журнал несёт строку об этом исходе с адресом пира
|
|
- **AND** значения заголовка в ней нет
|
|
|
|
#### Scenario: Сервис не ставит браузеру куки
|
|
|
|
- **GIVEN** адрес источника стоит в перечне доверенных
|
|
- **WHEN** запрос к адресу приложения проходит с заголовком
|
|
- **THEN** ответ не ставит браузеру ни куки сессии, ни иного значения доступа
|
|
|
|
#### Scenario: Значения заголовка нет в журнале
|
|
|
|
- **WHEN** запрос с заголовком `Remote-User` проходит через сервис
|
|
- **THEN** значение заголовка не встречается ни в одной журнальной записи
|
|
|
|
### Requirement: Учётная запись заводится первым обращением
|
|
|
|
Сервис SHALL заводить учётную запись при первом обращении с новым значением
|
|
`Remote-User` и MUST находить её по тому же значению при каждом следующем.
|
|
Значение MUST быть ключом учётной записи — уникальным и хранимым своей колонкой
|
|
таблицы пользователей.
|
|
|
|
Имя и адрес почты MUST браться из заголовков того же запроса, и только при
|
|
заведении. Оба MUST **приниматься**, а не браться как есть: имя обрезается по
|
|
пределу колонки и чистится от управляющих знаков, негодный адрес почты
|
|
отбрасывается. Негодное значение необязательного поля MUST не отменять
|
|
заведения записи — иначе человек с длинным именем у провайдера не завёлся бы
|
|
никогда, получая отказ сервиса на каждом запросе. Найденную запись повторное
|
|
обращение MUST не переписывать: иначе всякий запрос был бы записью в базу, а
|
|
правка имени у провайдера меняла бы карточку человека молча, посреди его работы.
|
|
|
|
Адрес почты MUST быть необязательным: провайдер не обязан его приносить, а ключом
|
|
он не служит. Ключом его брать нельзя вовсе — адрес меняется, и первое обращение
|
|
с чужим адресом досталось бы чужой записи.
|
|
|
|
**Ключ учётной записи MUST не правиться ничем, кроме заведения самим сервисом.**
|
|
Переписанный ключ отдаёт архив следующему, кто придёт с этим именем, а вернуть
|
|
его будет нечем — владелец записи назначается один раз и не меняется. Держится
|
|
это тем, что адреса, которым учётная запись правится снаружи, у сервиса нет
|
|
вовсе: своих экранов профиля он не заводит, а поверхности хранилища, правившей
|
|
запись библиотечным правилом, не осталось. Правку остаётся сделать запросом к
|
|
базе, и это работа владельца сервиса, а не спрашивающего.
|
|
|
|
Одновременные первые обращения одним значением MUST кончаться одной учётной
|
|
записью: уникальность держит схема, а не порядок обращений.
|
|
|
|
**Два отказа уникальности различаются, и исход у них разный.** Отказ по ключевой
|
|
колонке — это гонка двух первых обращений одним именем, и он MUST кончаться
|
|
повторным поиском и продолжением работы. Отказ по любой другой колонке — адрес
|
|
почты, пришедший от провайдера, уже занят другой учётной записью — MUST кончаться
|
|
заведением записи **без почты**: она необязательна. Без этого разреза второй
|
|
человек с общим почтовым ящиком не завёлся бы никогда, потому что повторный поиск
|
|
по имени снова ничего не находит.
|
|
|
|
Цена ключа называется целиком, обеими сторонами. Переименование пользователя у
|
|
провайдера заводит **новую** учётную запись, и записи прежней остаются у прежней;
|
|
слить их или убрать нечем — владелец записи не меняется, а учётная запись с
|
|
записями не удаляется по норме `storage`. **Логин же переиспользуем**: человек,
|
|
которому провайдер выдал логин ушедшего, при первом обращении попадает в
|
|
существующую запись и получает весь её архив. Не допускать переиспользования —
|
|
работа провайдера; сервису неизменяемого признака заголовок не приносит, и эта
|
|
цена принимается, а не обходится.
|
|
|
|
#### Scenario: Первое обращение заводит запись
|
|
|
|
- **GIVEN** учётной записи с этим значением ещё нет
|
|
- **WHEN** приходит запрос с заголовком `Remote-User`
|
|
- **THEN** учётная запись появляется
|
|
- **AND** запрос идёт от её имени
|
|
|
|
#### Scenario: Повторное обращение попадает в ту же запись
|
|
|
|
- **GIVEN** учётная запись заведена первым обращением
|
|
- **WHEN** приходит второй запрос с тем же значением заголовка
|
|
- **THEN** новой учётной записи не появляется
|
|
- **AND** запрос идёт от имени прежней
|
|
|
|
#### Scenario: Разным значениям — разные записи
|
|
|
|
- **WHEN** приходят запросы с двумя разными значениями заголовка
|
|
- **THEN** заводятся две учётные записи
|
|
- **AND** записи одного не видны другому
|
|
|
|
#### Scenario: Имя не переписывается вторым обращением
|
|
|
|
- **GIVEN** учётная запись заведена с одним значением `Remote-Name`
|
|
- **WHEN** приходит запрос с тем же `Remote-User` и другим `Remote-Name`
|
|
- **THEN** имя учётной записи остаётся прежним
|
|
|
|
#### Scenario: Два одновременных первых обращения дают одну запись
|
|
|
|
- **GIVEN** учётной записи с этим значением ещё нет
|
|
- **WHEN** два запроса с одним значением заголовка приходят одновременно
|
|
- **THEN** в таблице пользователей появляется ровно одна строка
|
|
- **AND** оба запроса идут от её имени
|
|
|
|
#### Scenario: Занятая почта не мешает завести запись
|
|
|
|
- **GIVEN** учётная запись с этим адресом почты уже заведена
|
|
- **WHEN** приходит первое обращение с другим `Remote-User` и тем же
|
|
`Remote-Email`
|
|
- **THEN** заводится своя учётная запись
|
|
- **AND** адреса почты у неё нет
|
|
|
|
#### Scenario: Адреса правки учётной записи у сервиса нет
|
|
|
|
- **GIVEN** человек узнан и его учётная запись заведена
|
|
- **WHEN** ищут адрес сервиса, которым он правит свою учётную запись
|
|
- **THEN** такого адреса нет
|
|
|
|
#### Scenario: Негодное имя не отменяет заведения
|
|
|
|
- **GIVEN** учётной записи с этим значением ещё нет
|
|
- **WHEN** приходит обращение с именем длиннее предела колонки
|
|
- **THEN** учётная запись заводится, а имя обрезано по пределу
|
|
|
|
#### Scenario: Негодная почта отбрасывается, а не отменяет заведение
|
|
|
|
- **GIVEN** учётной записи с этим значением ещё нет
|
|
- **WHEN** приходит обращение с адресом почты, не похожим на адрес
|
|
- **THEN** учётная запись заводится без почты
|
|
|
|
#### Scenario: Отвергнутый ограничителем запрос учётной записи не заводит
|
|
|
|
- **GIVEN** бюджет ограничителя частоты выбран
|
|
- **WHEN** приходит обращение с новым значением заголовка
|
|
- **THEN** ответ несёт отказ ограничителя
|
|
- **AND** учётной записи не появляется
|
|
|
|
#### Scenario: Заведение учётной записи видно в журнале
|
|
|
|
- **WHEN** приходит первое обращение с новым значением заголовка
|
|
- **THEN** журнал несёт строку о заведении с идентификатором записи
|
|
- **AND** значения заголовка в ней нет
|
|
|
|
### Requirement: Доверенный источник объявлен настройкой
|
|
|
|
Сервис SHALL брать перечень доверенных адресов из конфига и MUST ронять старт,
|
|
когда перечень пуст либо его строки не читаются как адрес или подсеть. Пустой
|
|
перечень значит «не верить никому»: сервис поднялся бы никого не узнающим, а
|
|
узнать об этом было бы неоткуда.
|
|
|
|
Отказ старта MUST называть имя ключа. Ни адресов провайдера, ни идентификатора
|
|
клиента, ни секрета клиента в конфиге MUST не быть: менять код больше не на что,
|
|
и секрет уходит из конфига вместе с протоколом.
|
|
|
|
Перечень MUST называться строкой журнала при подъёме. Сервис, никого не узнающий
|
|
из-за неверного перечня, иначе неотличим от сервиса, до которого заголовок не
|
|
доходит вовсе, — а это разные поломки в разных местах.
|
|
|
|
#### Scenario: Пустой перечень роняет старт
|
|
|
|
- **WHEN** сервис поднимается с пустым перечнем доверенных адресов
|
|
- **THEN** старт кончается отказом
|
|
- **AND** отказ называет имя ключа
|
|
|
|
#### Scenario: Негодная строка перечня роняет старт
|
|
|
|
- **WHEN** сервис поднимается с перечнем, где строка не читается как адрес или
|
|
подсеть
|
|
- **THEN** старт кончается отказом
|
|
|
|
#### Scenario: Перечень виден в журнале подъёма
|
|
|
|
- **WHEN** сервис поднимается с заполненным перечнем
|
|
- **THEN** журнал подъёма называет доверенные адреса
|