## Критерии приёмки Дословно из записи задачи `trusted-header-login`. Файл задачи закрытие удалит — критерии обязаны его пережить. 1. Обращение к адресу приложения с заголовком от доверенного источника идёт от имени учётной записи, заведённой при первом таком обращении, а повторное с тем же значением попадает в ту же запись. Оракул — тест обработчика: два запроса подряд, в хранилище одна запись пользователя. 2. Тот же заголовок с недоверенного адреса даёт `401`, а не вход под названным именем. Оракул — тест: запрос с адресом источника вне перечня доверенных. 3. Собственные адреса входа хранилища сессии не выдают и учётную запись не меняют. Оракул — тест по перечню адресов под `/api/collections/users/`: каждый отвечает отказом. 4. Механики OIDC в дереве не осталось: корня `/auth`, кук входа, `ApplyProviderSettings`, `cmd/oidcstub` и секрета клиента в конфиге. Оракул — поиск по этим именам плюс зелёный `task gate`. 5. Разграничение записей по владельцу работает как прежде: чужая запись неотличима от несуществующей. Оракул — существующие тесты владельца остаются зелёными. ## Рубрика ревью дизайна Двенадцать свойств, по которым судится узел этого рода — слой узнавания предъявителя плюс заведение учётной записи первым обращением. Порождены до чтения дизайна. Приёмка идёт по одному списку: эти пункты наравне с критериями постановки выше. 1. Доверие ограничено источником, и источник берётся у соединения, а не у пересылаемого значения; настройка проверяется на старте, небезопасного умолчания нет. 2. Заголовок узнаётся однозначно: более одного значения не даёт «первое попавшееся». 3. Вырожденное значение — пустое, пробельное, сверх предела длины, с управляющими знаками — не узнаёт никого и не заводит ничего. 4. Ключ учётной записи стабилен, и цена его нестабильности названа в обе стороны — и смена, и переиспользование. Выборка по ключу имеет свой индекс. 5. Заведение первым обращением идемпотентно и устойчиво к конкуренции; отказ уникальности не по ключевой колонке имеет назначенный исход. 6. Результат остаётся функцией уже произошедшего: ключевая колонка неизменяема после заведения. 7. Второго способа стать этим человеком нет, а при двух предъявленных удостоверениях победитель назначен нормой, а не порядком слоёв. 8. Закрытие поверхности не отменяет законного пути заведения записи самим сервисом. 9. Значение заголовка — недоверенный вход на всём пути: не в журнал, не в ответ, не в метку метрики, и в хранилище параметром, а не подстановкой в фильтр. 10. Охват слоя назван, перечень открытых адресов закрыт, посторонний заголовок ответа открытых адресов не меняет. 11. Срок узнавания назван: всё, что переживает запрос, либо отсутствует, либо имеет назначенный срок, и этот срок и есть задержка отзыва. 12. Исход узнавания наблюдаем владельцем и неразличим для отправителя. ## 1. Схема хранилища - [x] 1.1 Новый шаг схемы `202608220001_trusted_header_login.go`: колонка `provider_login` в коллекции `users` с уникальным индексом, почта переведена в необязательную, `OAuth2.Enabled = false` и `OAuth2.Providers = nil`. Применённый `202608120001_oidc_login` не трогается. - [x] 1.2 Тот же шаг снимает **все** правила доступа коллекции `users` в пустое — `ListRule`, `ViewRule`, `CreateRule`, `UpdateRule`, `DeleteRule`. Сегодня четыре из них библиотечные (`id = @request.auth.id`), и правка своей записи открыта: это путь захвата чужого имени через ключевую колонку. - [x] 1.3 Шаг зарегистрирован в `migrations.go`, имя ключевой колонки объявлено константой рядом с именами коллекций. - [x] 1.4 `schema_test.go` сторожит `users` наравне с шестью коллекциями, которые он уже проверяет: все пять правил пусты. - [x] 1.5 Откат шага возвращает то, что было до него, кроме открытого создания записи и открытой правки, и не падает на проверке коллекции. - [x] 1.6 `docs/database.md` описывает новую колонку, снятые настройки и снятые правила доступа. ## 2. Настройка - [x] 2.1 Секция входа конфига переписана: адреса провайдера, идентификатор и секрет клиента, адрес возврата и признак защищённой куки убраны, перечень доверенных адресов заведён: секция `[auth]` остаётся под своим именем, в ней один ключ `trusted_proxies` (решение человека 2026-08-22). - [x] 2.2 Проверка настройки на старте: пустой перечень и нечитаемая строка роняют старт с именем ключа. - [x] 2.3 `config.example.toml` переписан вместе с секцией, раздел про локальный вход заменён на подставной прокси. ## 3. Узнавание пришедшего - [x] 3.1 Слой узнавания в транспорте HTTP: читает заголовок, судит адрес пира по перечню, ставит учётную запись предъявителя только когда её ещё нет. Вешается корневым, но действует на объявленной области — корень приложения плюс адрес выдачи файлового токена; область выводится из перечня адресного пространства, а не пишется вторым списком. - [x] 3.2 Приём значения заголовка: пустое и пробельное не узнают никого, более одного значения не узнаёт никого, предел длины и отказ на управляющие знаки, сравнение точное. Значение уходит в хранилище параметром, а не подстановкой в текст фильтра. - [x] 3.3 Поиск и заведение учётной записи — **методом пакета хранилища**, а не куском в транспорте: у правила один дом, и `api-tokens` возьмёт его же. Имя и почта берутся только при заведении, найденная запись не переписывается; отказ уникальности по ключевой колонке ведёт к повторному поиску, по любой другой — к заведению записи без почты. - [x] 3.4 Отказ хранилища при узнавании кончается отказом сервиса, а не молчаливым проходом неузнанным. - [x] 3.5 Журнал: исходы «заголовок с недоверенного адреса» и «учётная запись заведена» — с адресом пира и идентификатором записи, без значения заголовка; перечень доверенных адресов называется строкой при подъёме. - [x] 3.6 `ApplyProviderSettings`, `ProviderSettings` и `ProviderName` убраны из пакета хранилища; `SessionDuration` убран вместе со сроком сессии. ## 4. Снос механики OIDC - [x] 4.1 `internal/controller/http/auth.go` удалён целиком вместе с корнем `/auth` в перечне адресного пространства. - [x] 4.2 `SessionFromCookie` и `BlockSessionRefresh` удалены; места их привязки переписаны на новый слой. - [x] 4.3 `cmd/oidcstub` удалён. - [x] 4.4 Приложение: адрес, которым экран уводил ко входу, убран; неузнавание показывается строкой и отличается от прочей неудачи. Юнит-тесты приложения обновлены. ## 5. Способ представиться без прокси - [x] 5.1 Заведён `cmd/devtools` с подкомандой `proxy`: слушает свой порт, ставит заголовок, переправляет запрос сервису. Подкоманду `admin` заводит задача `dev-run-task`. В образ пакет не едет: строка сборки `Dockerfile` называет точку входа поимённо. - [x] 5.2 `CLAUDE.md`, раздел «Команды» и раздел «Запреты» — про локальный вход без прокси; `README.md`, если он про это говорит. ## 6. Проверки - [x] 6.1 Тест: первое обращение с доверенного адреса заводит учётную запись, второе с тем же значением попадает в ту же — в хранилище одна запись (критерий 1). - [x] 6.2 Тест: тот же заголовок с недоверенного адреса даёт `401`, и учётной записи не появляется (критерий 2). - [x] 6.3 Тест по перечню собственных адресов входа хранилища под `/api/collections/users/`: каждый отвечает отказом и учётной записи не меняет (критерий 3). - [x] 6.4 Тест: годный собственный токен побеждает заголовок, а протухший узнаванию не мешает. - [x] 6.4a Тест: узнанный не правит свою запись в коллекции пользователей запросом к хранилищу и не перечисляет коллекцию — путь захвата чужого имени закрыт. - [x] 6.4b Тест: пустой заголовок, два значения одного заголовка и значение сверх предела длины не узнают никого и записи не заводят. - [x] 6.4c Тест: два одновременных первых обращения одним значением дают одну запись. - [x] 6.4d Тест: занятый адрес почты не мешает завести запись — она заводится без почты. - [x] 6.4e Тест: запрос с заголовком на `GET /health` учётной записи не заводит. - [x] 6.4f Тест: прежние адреса под корнем `/auth` отдают разметку приложения. - [x] 6.5 Тест: значение заголовка не попадает в журнал. - [x] 6.6 Тест: ответ на успешный запрос не ставит браузеру куки. - [x] 6.7 Тест: токен файла выдаётся узнанному по заголовку, и путь к файлу записи проходит целиком. - [x] 6.8 Существующие тесты владельца и разграничения переписаны на новый способ представиться и остаются зелёными (критерий 5). - [x] 6.9 Тесты входа, обмена кода, куки, продления и выхода удалены вместе с предметом. ## 7. Документы и закрытие - [x] 7.1 `docs/security.md`: периметр, недоверенный вход, разграничение доступа; четвёртый сдвиг про секрет в базе снят; требование к контуру — прокси **ставит** заголовок, а не пропускает пришедший — названо поимённо; там же цена переиспользования логина у провайдера. - [x] 7.2 `CLAUDE.md`: изъятие из инварианта о секрете снято. - [x] 7.3 `docs/architecture.md` приведён в соответствие; в «Единые точки проекта» добавлена строка про дом правила узнавания предъявителя. - [x] 7.3a `docs/passport.md`: строки про сессию OIDC у потребителей и в границе «Управление учётными записями» — учётную запись сервис не заводит по своей воле, а зеркалит имя, названное провайдером. - [x] 7.4 Поиск по `oidc`, `transcriber_session`, `transcriber_login`, `ApplyProviderSettings`, `/auth/` в дереве не находит живого кода (критерий 4). - [x] 7.5 `task gate` зелёный, кроме унаследованного `hadolint DL3066` — отказ воспроизводится на чистом `origin/master`, объявлен долгом в `CLAUDE.md` (критерий 4). ## 8. Отработка ревью кода - [x] 8.1 Слой узнавания переехал за ограничитель частоты: отвергнутый запрос больше не заводит учётной записи. - [x] 8.2 Заведение учётной записи пишется в журнал; два значения заголовка — предупреждением, а не отладочным уровнем. - [x] 8.3 Имя и почта принимаются, а не кладутся как есть: длинное имя больше не запирает человека вечным отказом сервиса. - [x] 8.4 Отказ уникальности судится по коду, а не по имени колонки: негодная почта отличима от занятой. - [x] 8.5 Предел логина считается в знаках, а не в байтах. - [x] 8.6 Уникальный индекс по ключу сделан частичным — как соседний индекс почты; подъём на непустой базе больше не роняет накатку. - [x] 8.7 Ключ учётной записи закрыт от правки рукой в панели модельным хуком. - [x] 8.8 Хранилищу назван заголовок адреса спрашивающего: бюджет ограничителя перестал быть общим на весь сервис. - [x] 8.9 Приложение показывает текст сервера, своего словаря под коды ответа не заводит; текст `401` больше не зовёт ко входу, которого нет. - [x] 8.10 Оснастка берёт имена заголовков константами транспорта. - [x] 8.11 Три записи конвенций приведены к действительности; словарь полей журнала пополнен, изъятие про вывод оснастки записано. - [x] 8.12 Порог отката образа назван в «Эксплуатации»; требование к контуру про `X-Forwarded-For` и цена ширины перечня — в модели угроз. - [x] 8.13 Образец конфига сужен до адреса прокси. - [x] 7.6 Поведенческая проверка: сервис поднят локально, приложение открывается, учётная запись заводится первым обращением, запрос без заголовка получает отказ.