вход переехал на доверенный заголовок Authelia вместо собственного OIDC

- пришедшего называет заголовок Remote-User от прокси, и верят ему только с
  адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни
  корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе
- учётная запись заводится первым обращением: EnsureUser в пакете хранилища,
  шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users
- cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт
  унаследованный DL3066 — пользователь образа назван числом
This commit is contained in:
av
2026-08-22 20:24:22 +03:00
parent e4441f3c49
commit 7f33c957e5
63 changed files with 5257 additions and 2305 deletions
@@ -0,0 +1,206 @@
## Критерии приёмки
Дословно из записи задачи `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 Поведенческая проверка: сервис поднят локально, приложение открывается,
учётная запись заводится первым обращением, запрос без заголовка получает
отказ.