Files
transcriber/openspec/changes/archive/2026-08-23-config-test-headers-login/specs/access/spec.md
T
av 52fe31319a локальный вход задаётся конфигом: заголовки подставляет сам сервис
- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug:
  заголовки входа подставляет слой транспорта, второго процесса локальный запуск
  больше не требует
- подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает
  сам сервис
- адресного предохранителя нет по решению владельца — цена названа в ADR и в
  модели угроз
2026-08-23 13:12:47 +03:00

509 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## ADDED Requirements
### Requirement: Отладочный запуск называет пришедшего настройками
Сервис SHALL подставлять запросу заголовки входа значениями из секции
`[auth] test_headers`, когда включён предохранитель `[server] debug`, и MUST
делать это так, чтобы узнавание не отличало подставленный заголовок от
поставленного обратным прокси. Ветка кода, которой узнаётся пришедший, обязана
быть той же, что работает в бою: отладке подлежит боевой путь, а не его
отладочный двойник.
Подстановка MUST происходить только при **отсутствии** заголовка `Remote-User` в
запросе. Заголовок, пришедший в любом числе значений, MUST оставаться нетронутым:
узнавание отвергает запрос с более чем одним значением, и подстановка, дописавшая
второе, превратила бы законный отказ в проход.
Подстановка MUST происходить только тогда, когда адрес соединения попадает в
перечень `[auth] trusted_proxies`, и адрес этот MUST судиться **той же
функцией**, какой судит его узнавание: и разбор адреса пира, и сверка с
перечнем берутся из одного дома, а не пишутся вторым списком. Второй перечень
разошёлся бы с первым молча — так же, как разошлись бы два списка имён
заголовков. Второго барьера у подстановки нет и не заводится: круг тех, кто
вправе назвать пришедшего, уже очерчен этим перечнем.
Подстановка MUST действовать только при **непустой** секции имитации. Пустая
секция значит «подставлять нечего»: цепочка слоёв при ней та же, что при
выключенном предохранителе, и ни одного заголовка запроса слой не трогает.
Иначе включённый предохранитель при пустой секции срезал бы `Remote-Email` у
запроса, пришедшего без `Remote-User`, — поведение, которого в бою нет.
Подставляя, сервис MUST распоряжаться всей тройкой заголовков `Remote-*`
целиком: названные секцией ставятся её значением, не названные удаляются.
Запрос без `Remote-User`, но с `Remote-Email` иначе собрал бы личность из двух
источников — логин свой, почту чужую.
Подстановка MUST действовать только под корнем приложения — там же, где действует
узнавание. Проба здоровья, метрики и раздача приложения её не видят.
#### Scenario: Запрос без заголовка входа получает имя из настроек
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
- **WHEN** запрос приходит с доверенного адреса без единого заголовка `Remote-*`
- **THEN** сервис узнаёт пришедшего под именем из секции
- **AND** учётная запись заводится, если её не было
#### Scenario: Пришедший заголовок не подменяется
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
- **WHEN** запрос приходит с доверенного адреса с заголовком `Remote-User`
- **THEN** узнавание получает значение из запроса, а не из настроек
#### Scenario: Два значения заголовка остаются отказом
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
- **WHEN** запрос приходит с двумя значениями заголовка `Remote-User`
- **THEN** сервис не подставляет ничего, и запрос остаётся неузнанным
#### Scenario: Недоверенный адрес имени не получает
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
- **WHEN** запрос без заголовка приходит с адреса вне перечня доверенных
- **THEN** сервис не подставляет ничего, и запрос остаётся неузнанным
#### Scenario: Чужой заголовок почты не смешивается со своим логином
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User` и не
называет `Remote-Email`
- **WHEN** запрос приходит без `Remote-User`, но с заголовком `Remote-Email`
- **THEN** узнавание получает логин из настроек и не получает адреса почты вовсе
#### Scenario: Пустая секция имитации заголовков не трогает
- **GIVEN** предохранитель включён, секция имитации пуста
- **WHEN** запрос приходит с доверенного адреса без `Remote-User`, но с
заголовком `Remote-Email`
- **THEN** слой заголовков не трогает, и `Remote-Email` доходит до узнавания
нетронутым
#### Scenario: Выключенный предохранитель имён не раздаёт
- **GIVEN** предохранитель выключен, секция имитации пуста
- **WHEN** запрос приходит с доверенного адреса без заголовка `Remote-User`
- **THEN** запрос остаётся неузнанным
#### Scenario: Наблюдение подстановки не видит
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
- **WHEN** запрос приходит на `GET /health` без заголовка
- **THEN** ответ тот же, что и при выключенном предохранителе
### Requirement: Настройка, открывающая вход всем, роняет старт
Сервис SHALL отказываться подниматься на настройках, при которых отладочный вход
становится открытым входом, и MUST называть в отказе имя ключа. Проверка идёт на
старте, до приёма трафика: настройка, отданная на честность выкладки, проверяется
только тем, что чужой архив уже уехал не тому.
Умолчания названы нормой, а не образцом конфига. Отсутствие ключа `[server]
debug` MUST читаться как выключенный предохранитель, а отсутствие секции
`[auth] test_headers` — как пустая секция: конфиг сегодняшнего дня, не тронутый
ни на байт, обязан вести себя ровно как вёл. Ошибка разбора значения MUST
кончаться отказом старта, а не прочтением «включено».
Секция имитации MUST считаться **непустой**, как только в ней назван хотя бы
один ключ, каким бы ни было его значение. `Remote-User = ""` — заполненная
секция, а не пустая: человек, написавший ключ, имитацию завёл, и пустое значение
у него — вторая поломка, а не отсутствие первой.
Отказом MUST быть каждый из четырёх случаев.
Первый — секция имитации заполнена при выключенном предохранителе. Состояние это
не читается никак: либо человек забыл включить предохранитель и будет искать
поломку везде, кроме одного ключа, либо забыл убрать имитацию из боевого файла — и
тогда до открытого архива остаётся одно слово. Молчаливое игнорирование и
предупреждение в журнале оба оставляют вторую поломку жить.
Второй — секция имитации называет ключ, не совпадающий ни с одним заголовком
входа, который сервис читает. Перечень принимаемых имён MUST порождаться теми же
именами заголовков, которыми пользуется узнавание, а не перечисляться вторым
списком; сравнение MUST идти по каноническому виду имени, потому что в HTTP имя
нечувствительно к регистру, а ключ конфига чувствителен. Опечатка в имени иначе
кончается сервисом, который никого не узнаёт, без единого следа.
Третий — секция имитации непуста, а годного `Remote-User` в ней нет. Ключ
логина MUST быть назван: секция, называющая один `Remote-Email`, поднимает
сервис, который подставит почту, удалит логин и не узнает никого. Значение
логина MUST проходить **тот же приём**, каким узнавание судит пришедшее
значение (`internal/entity.AcceptProviderLogin`): пустое, из одних пробельных
знаков, сверх предела длины и с управляющими знаками — отказ старта. Иначе
сервис поднимается, ставит заголовок, получает отказ приёма и отвечает
неузнанным на всё, оставляя за собой одну отладочную строку. Оба состояния — то
самое «не читается никак», ради которого заведён первый случай.
Четвёртый — два ключа секции дают одно каноническое имя заголовка. `Remote-User`
и `remote-user` для TOML — два ключа, для HTTP — одно имя: сервис подставил бы
одно значение, а второе потерял бы молча, и человек искал бы поломку в значении,
которого сервис не читал вовсе. Отказ MUST называть секцию и причину, а значений
MUST не называть.
Включённый предохранитель при пустой секции имитации отказом MUST не быть: сам по
себе он ничего не включает.
#### Scenario: Имитация без предохранителя не поднимается
- **GIVEN** секция имитации заполнена, предохранитель выключен
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет имя ключа предохранителя
#### Scenario: Неизвестное имя заголовка не поднимается
- **GIVEN** предохранитель включён, секция имитации называет ключ, которого нет
среди читаемых заголовков входа
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет принимаемые имена
#### Scenario: Имитация без ключа логина не поднимается
- **GIVEN** предохранитель включён, секция имитации называет только
`Remote-Email`
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет имя недостающего ключа логина
#### Scenario: Два ключа одного заголовка не поднимаются
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User` и
`remote-user`
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет секцию, в которой два ключа дали одно имя
заголовка
#### Scenario: Негодное значение логина не поднимается
- **GIVEN** предохранитель включён, секция имитации называет `Remote-User`
значением в 300 знаков
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет имя ключа, а не его значение
#### Scenario: Пустое значение логина считается заполненной имитацией
- **GIVEN** предохранитель выключен, секция имитации называет `Remote-User = ""`
- **WHEN** сервис запускают
- **THEN** старт отказывает и называет имя ключа предохранителя
#### Scenario: Конфиг без ключа предохранителя ведёт себя как с выключенным
- **GIVEN** конфиг не называет ни `[server] debug`, ни секции имитации
- **WHEN** сервис запускают
- **THEN** сервис поднимается, подстановки не заводит и никого сам не называет
#### Scenario: Предохранитель без имитации поднимается
- **GIVEN** предохранитель включён, секция имитации пуста
- **WHEN** сервис запускают
- **THEN** сервис поднимается
### Requirement: Отладочный запуск виден в журнале
Сервис SHALL сообщать владельцу о включённой подстановке заголовков строкой
журнала при старте и MUST не печатать при этом ни одного подставляемого значения.
Сервис, называющий пришедшего сам, — это ровно то «может стать проблемой», ради
которого заведён предупреждающий уровень: строка старта идёт на `WARN`.
Строка старта MUST нести имена подставляемых заголовков и MUST не нести их
значений. Логин — ключ к чужому архиву, и запрет на печать значения, дающего
доступ, действует на подставленное значение наравне с пришедшим.
Запрос, которому заголовки подставлены, SHALL писаться на уровне `INFO`:
этой строкой «узнан подстановкой» отличается от «узнан прокси». Боевой журнал
она не топит по построению — пишется только там, где подстановка работает, а в
бою предохранитель выключен умолчанием; на отладочном уровне её не увидел бы
никто, потому что настройки под уровень журнала у сервиса нет и он зашит `INFO`.
Строка MUST нести адрес пира и не нести подставленных значений.
Запрос, которому подставить нельзя из-за недоверенного адреса, MUST оставлять
**свою** строку предупреждающим уровнем. Узнавание на этом месте предупреждения
не пишет: подстановка работает при отсутствии `Remote-User`, а отсутствие
заголовка узнавание считает случаем штатным и пишет о нём отладочную строку;
предупреждение о недоверенном адресе оно бережёт для заголовка, который
**пришёл**. Без своей строки самый частый локальный отказ — браузер пришёл
с `::1`, а перечень называет `127.0.0.1` — не отличим от поломки узнавания:
человек видит `401` на всём приложении, заполненную секцию имитации и
включённый предохранитель. Строка MUST нести адрес пира и MUST не нести
подставляемых значений.
#### Scenario: Старт с подстановкой предупреждает владельца
- **GIVEN** предохранитель включён, секция имитации заполнена
- **WHEN** сервис поднимается
- **THEN** журнал несёт предупреждение с именами подставляемых заголовков
#### Scenario: Подставленного значения нет в журнале
- **GIVEN** предохранитель включён, секция имитации называет логин и адрес почты
- **WHEN** сервис поднимается и принимает запрос без заголовка
- **THEN** ни логин, ни адрес почты не встречаются ни в одной журнальной записи
#### Scenario: Отказ подстановки на недоверенном адресе виден в журнале
- **GIVEN** предохранитель включён, секция имитации заполнена
- **WHEN** запрос без заголовка `Remote-User` приходит с адреса вне перечня
доверенных
- **THEN** журнал несёт предупреждение с адресом пира
- **AND** подставляемых значений в строке нет
#### Scenario: Подстановка на запросе видна при боевом уровне журнала
- **GIVEN** предохранитель включён, секция имитации заполнена
- **WHEN** запрос с доверенного адреса получает подставленные заголовки
- **THEN** строка об этом имеет уровень `INFO` и видна журналу, настроенному
по-боевому
### Requirement: Предохранитель отладки включает только подстановку заголовков
Предохранитель `[server] debug` SHALL менять одно поведение сервиса — подстановку
заголовков входа — и MUST не менять никакого другого. Перечень следствий закрыт, и
новое следствие вешается на этот ключ только отдельным требованием спеки: ключ,
названный общим словом, иначе обрастает всем подряд, и выключить его перестаёт
означать «сервис ведёт себя как в бою».
Сам по себе включённый предохранитель не включает и подстановки: она MUST
действовать только при непустой секции имитации, и при пустой цепочка слоёв MUST
быть та же, что при выключенном предохранителе, — требование «Отладочный запуск
называет пришедшего настройками».
Включённый предохранитель MUST не менять уровень журнала, не добавлять в ответы
тексты внутренних отказов и следы стека, не выводить тела запросов и ответов
внешних сервисов, не снимать и не ослаблять ограничитель частоты, не подменять
распознаватель, не ослаблять ни одной проверки старта и не открывать неузнанному
ни одного адреса.
#### Scenario: Включённый предохранитель без имитации ничего не меняет
- **GIVEN** предохранитель включён, секция имитации пуста
- **WHEN** сервис принимает запросы
- **THEN** он ведёт себя ровно так же, как с выключенным предохранителем
#### Scenario: Ограничитель частоты работает и в отладочном запуске
- **GIVEN** предохранитель включён, секция имитации заполнена
- **WHEN** запросы идут чаще дозволенного
- **THEN** ограничитель частоты режет их так же, как при выключенном
предохранителе
## MODIFIED Requirements
### Requirement: Кого пускать, решает провайдер
Сервис SHALL пускать всякого, кого назвал доверенный источник, и своей проверки
допуска MUST не делать. Кто допущен, определяет правило провайдера на домен
сервиса — настройка выкладки, лежащая вне репозитория.
Требование записано именно как решение с ценой, а не как умолчание: провайдер
общий для контура, и правило, настроенное слишком широко, открывает сервис
всякому, у кого есть учётная запись у провайдера. Проверить это по коду нельзя,
поэтому граница названа здесь и повторена в модели угроз.
Цена сдвинулась в нашу пользу: провайдер судит **каждый** запрос, а не только
первый. Прежде сервис спрашивал провайдера однажды и потом верил выданному
значению до его истечения — отзыв доступа доходил до сервиса с задержкой в срок
жизни этого значения. Теперь отзыв действует со следующего запроса.
Обратная сторона у этого одна, и она названа прямо: **весь барьер держится на
том, что прокси ставит заголовок сам, а не пропускает пришедший**. Прокси,
пропускающий чужой заголовок, открывает сервис всякому под любым именем.
Требование к контуру записано в модели угроз; репозиторием оно не проверяется.
**Изъятие одно — отладочный запуск.** При включённом предохранителе
`[server] debug` доверенным источником становятся настройки сервиса: сервис
называет пришедшего сам, никого не спросив. Это единственное место, где имя
берётся не от провайдера. Требования к нему стоят выше отдельными строками, и
снимать их поодиночке нельзя: барьер держится всеми разом.
Держится изъятие **умолчанием, а не машиной**: предохранитель по умолчанию
выключен, а заполненная имитация без него роняет старт. Боевой перечень
доверенных адресов включению предохранителя не мешает — сервис, поднятый в бою с
включённым предохранителем и заполненной имитацией, назовёт своим именем всякого,
чей запрос пришёл через обратный прокси, то есть всякого, кто пришёл обычным
путём.
#### Scenario: Названный провайдером получает доступ
- **WHEN** запрос приходит с доверенного адреса с заголовком, поставленным
прокси
- **THEN** доступ открывается, а учётная запись заводится, если её не было
- **AND** сервис не спрашивает у заголовков ничего сверх имени, имени для показа
и адреса почты
#### Scenario: Отзыв у провайдера действует со следующего запроса
- **GIVEN** человек работал в сервисе, и провайдер закрыл ему доступ
- **WHEN** приходит следующий его запрос
- **THEN** прокси заголовка не ставит, и запрос получает отказ
#### Scenario: Без отладочного запуска имя приходит только от провайдера
- **GIVEN** предохранитель отладки выключен
- **WHEN** запрос приходит с доверенного адреса без заголовка
- **THEN** сервис никого не узнаёт и своего имени пришедшему не назначает
### Requirement: Пришедшего называет доверенный источник
Сервис SHALL узнавать пришедшего по заголовку `Remote-User`, который ставит
обратный прокси, сходивший к провайдеру, и MUST не вести собственного входа: ни
адреса, уводящего к провайдеру, ни адреса возврата, ни куки, ни выхода у сервиса
не остаётся.
**Изъятие одно — отладочный запуск.** При включённом предохранителе
`[server] debug` заголовок входа ставит не прокси, а сам сервис значением из
секции `[auth] test_headers`; узнавание при этом остаётся тем же и подставленного
заголовка от пришедшего не отличает. Условия, при которых источник этот законен,
и проверки старта, которыми он держится, стоят требованиями «Отладочный запуск
называет пришедшего настройками», «Настройка, открывающая вход всем, роняет
старт», «Отладочный запуск виден в журнале» и «Предохранитель отладки включает
только подстановку заголовков». Держится изъятие умолчанием предохранителя
«выключено» и отказом старта при заполненной имитации без него; боевой перечень
доверенных адресов включению предохранителя не мешает.
Заголовку сервис 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** значение заголовка не встречается ни в одной журнальной записи