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

264 lines
26 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.
## 0. Чекпоинт человека
Чекпоинт пройден: развилок не осталось, и решения владельца записаны здесь и в
`design.md`.
- [x] 0.1 Имена ключей конфига подтверждены дословно — `[server] debug` и
`[auth.test_headers]` (необратимое: имя ключа конфига)
- [x] 0.2 Форма имён в секции имитации подтверждена: ключ — имя заголовка, набор
принимаемых имён порождается константами транспорта, неизвестный ключ
роняет старт (`design.md`, решение 6)
- [x] 0.3 Адресного предохранителя не делаем — «полагаемся только на параметр
debug». Требование петлевого перечня доверенных адресов при включённом
предохранителе снято вместе с предикатом «петлевая запись»
(`design.md`, решение 4)
- [x] 0.4 Подкоманда `cmd/devtools proxy` убирается целиком (`design.md`,
решение 10)
- [x] 0.5 Критерии приёмки подтверждены владельцем; пункты 2 и 7 переписаны под
решение 0.3
## 1. Настройки
- [x] 1.1 Завести в `ServerConfig` ключ предохранителя с умолчанием «выключено» в
`defaultConfig()`; отсутствие ключа в конфиге читается как «выключено», а
негодное значение роняет старт, а не читается как «включено»
- [x] 1.2 Завести в `AuthConfig` подсекцию имитации заголовков в форме, выбранной
шагом 0.2; умолчание — пустая секция. Непустой секция считается при наличии
хотя бы одного ключа, каким бы ни было его значение: `Remote-User = ""`
заполненная секция
- [x] 1.3 Вывести перечень принимаемых имён заголовков из констант транспорта
`internal/controller/http/identity.go`, а не вторым списком; сравнение по
каноническому виду имени
- [x] 1.4 Завести проверку старта, охватывающую обе секции, методом на корневой
`Config` (`design.md`, решение 8), и звать её из `cmd/transcriber` рядом с
имеющимися проверками
- [x] 1.5 Отказ старта при заполненной имитации и выключенном предохранителе;
текст называет имя ключа предохранителя
- [x] 1.6 Отказ старта при неизвестном имени заголовка в секции; текст называет
принимаемые имена
- [x] 1.7 Отказ старта при непустой секции без годного `Remote-User`: ключ не
назван либо его значение не проходит `internal/entity.AcceptProviderLogin`
(пустое, пробельное, сверх предела длины, с управляющими знаками); текст
называет имя ключа и не называет значения
- [x] 1.8 Включённый предохранитель при пустой секции старт не роняет. Перечень
доверенных адресов старту при этом не судья: боевая запись перечня
включению предохранителя не мешает (решение 0.3)
## 2. Подстановка на входе
- [x] 2.1 Завести слой транспорта, подставляющий заголовки, в
`internal/controller/http`
- [x] 2.2 Поставить слой в цепочку корня приложения между ограничителем частоты и
узнаванием; при выключенном предохранителе и при пустой секции имитации
слой в цепочку не встаёт вовсе — цепочка та же, что сегодня
- [x] 2.3 Подставлять только при отсутствии `Remote-User`; запрос с любым числом
значений проходит нетронутым
- [x] 2.4 Подставлять только при попадании адреса пира в перечень доверенных, и
судить адрес **той же функцией**, какой судит узнавание (`peerAddress` и
сверка с перечнем из `internal/controller/http/identity.go`), а не вторым
списком
- [x] 2.5 Распоряжаться всей тройкой `Remote-*`: названные секцией ставить,
не названные удалять
- [x] 2.6 Проверить, что `TrustedHeaderIdentity` не изменён: отлаживается боевая
ветка узнавания
## 3. Журнал
- [x] 3.1 Предупреждение при старте с включённой подстановкой: имена заголовков,
поле `capability`, без значений
- [x] 3.2 Строка уровня `INFO` на запросе с подставленными заголовками: адрес
пира, `capability`, `transport`, без значений. Уровень боевой, и боевой
журнал строка не топит: пишется она только там, где подстановка работает,
а в бою предохранитель выключен умолчанием
- [x] 3.3 Предупреждение на запросе, которому подставить нельзя из-за
недоверенного адреса: адрес пира, `capability`, `transport`, без
подставляемых значений. Узнавание на этом месте молчит — заголовка нет, и
оно пишет отладочную строку
## 4. Оснастка
- [x] 4.1 Удалить подкоманду `proxy` из `cmd/devtools`: ветку `case "proxy"` в
`main()`, функцию `runProxy` и её флаги `-listen`, `-target`, `-user`,
`-name`, `-email`; неиспользуемые импорты уходят вместе с ней
- [x] 4.2 Править шапку пакета `cmd/devtools/main.go`: она говорит «Подкоманд
две: `proxy` — подставной обратный прокси, `resume` — …» и объясняет, зачем
прокси нужен. Остаётся одна подкоманда. Абзац про имена заголовков
константами вместе с прокси теряет предмет — оснастка их больше не читает
- [x] 4.3 Править `usage()` в `cmd/devtools/main.go`: убрать строку
`proxy подставной обратный прокси…`
- [x] 4.4 Убрать упоминания подкоманды из документов. Адреса — все, что нашёл
`grep -rn "devtools proxy"`, кроме каталога этого change и архива
`openspec/changes/archive/`:
- `README.md`, блок локального запуска («Локально прокси нет…» и команда
`go run ./cmd/devtools proxy`) — содержимое пишет шаг 6.5;
- `CLAUDE.md`, раздел «Команды», строка `go run ./cmd/devtools proxy`, и
раздел «Запреты», абзац «Локальный запуск не ходит наружу» — содержимое
пишет шаг 6.4;
- `config.example.toml`, блок «Локальный вход без Authelia» с двумя
вызовами подкоманды — содержимое пишут шаги 6.1 и 6.2;
- `docs/conventions/config.md`, абзац про закомментированный петлевой
адрес «под подставной прокси `cmd/devtools proxy`» — содержимое пишет
шаг 6.3;
- `docs/architecture.md`, строка таблицы «Оснастка владельца |
`cmd/devtools`»: «Подставной прокси для местного запуска и возврат
остановленной записи в работу» — содержимое пишет шаг 6.7;
- `docs/security.md`, перечень вне периметра, пункт «Машина разработчика и
то, что он на ней поднимает»: «её подкоманда `proxy` встаёт на место
контура» — содержимое пишет шаг 6.6;
- `docs/review.md`, запись о единственном доме подставных внешних
собеседников: «на их месте `cmd/devtools proxy`». Документ процессный, и
запись историческая: правится строкой о том, что прокси убран задачей
`config-test-headers-login`, а не переписывается
## 5. Тесты
- [x] 5.1 Тесты проверки старта по требованию «Настройка, открывающая вход всем,
роняет старт»: три отказа (имитация без предохранителя; неизвестное имя
заголовка; непустая секция без годного `Remote-User` — не названного и в
300 знаков) и законные случаи (предохранитель без имитации; конфиг, не
называющий ни одного нового ключа)
- [x] 5.2 Тесты слоя подстановки по сценариям требования «Отладочный запуск
называет пришедшего настройками», включая два значения `Remote-User`,
недоверенный адрес и чужой `Remote-Email`
- [x] 5.3 Тест: подставленное значение не встречается ни в одной журнальной
записи
- [x] 5.4 Тест: `GET /health` при включённом предохранителе отвечает так же, как
при выключенном
- [x] 5.5 Тест: ограничитель частоты работает при включённом предохранителе
- [x] 5.6 Тест: при включённом предохранителе и пустой секции запрос без
`Remote-User`, но с `Remote-Email` доходит до узнавания нетронутым
- [x] 5.7 Тест: запрос без заголовка с недоверенного адреса при включённом
предохранителе оставляет предупреждение с адресом пира
- [x] 5.8 Тест: конфиг без новых ключей даёт ту же цепочку слоёв и те же ответы,
что до изменения
## 6. Документы
- [x] 6.1 `config.example.toml`: обе новые настройки с комментарием — зачем,
допустимые значения и цена включения. Форма названа прямо: боевой перечень
доверенных адресов остаётся рабочим значением, `[server] debug` стоит рабочей строкой
`false`, секция `[auth.test_headers]` целиком закомментирована, а петлевые
адреса в перечне стоят комментарием **парой**`127.0.0.1` и `::1`:
браузер разрешает `localhost` в IPv6 не реже, чем в IPv4, и перечень без
`::1` даёт неузнанный запрос без единой понятной строки
- [x] 6.2 Записать рецепт локального входа одним связным блоком — списком правок
сверху вниз, а не тремя комментариями по месту: (1) добавить в перечень
доверенных адресов пару петлевых, (2) поставить `debug = true`,
(3) раскомментировать секцию имитации и назвать в ней `Remote-User`. Дом
блока — `config.example.toml`, и та же последовательность повторена ссылкой
в `README.md` и `CLAUDE.md`
- [x] 6.3 `docs/conventions/config.md`: локальный вход описан настройками; абзац
про закомментированный петлевой адрес под подставной прокси приведён в
соответствие с формой шага 6.1 (пара петлевых адресов, закомментированная
секция имитации, рецепт одним блоком) — строкой «*Расхождение:*», если
форма образца расходится с общим правилом секции
- [x] 6.4 `CLAUDE.md`: раздел «Запреты», абзац о локальном запуске — вход идёт
настройками, второго процесса нет; раздел «Команды», строка подкоманды
`proxy` убирается
- [x] 6.5 `README.md`: инструкция локального запуска
- [x] 6.6 `docs/security.md`, «Периметр»: изъятие отладочного запуска, чем оно
держится (умолчание предохранителя, отказ старта при имитации без него,
шаблон выкладки) и что боевая поломка машиной не исключена. Там же —
пункт «Машина разработчика и то, что он на ней поднимает»: подкоманды
`proxy` больше нет
- [x] 6.7 `docs/architecture.md`: строка таблицы про оснастку разработчика (без
прокси) и новый слой в перечне слоёв транспорта
- [x] 6.8 `docs/passport.md`, «Управление учётными записями»: сегодня раздел
утверждает, что кто пришёл, сервис не решает никогда, и что исключений у
этого больше нет. Назвать изъятие одной строкой с его границей —
«держится предохранителем `[server] debug`, выключенным по умолчанию» — и
ссылкой на спеку `access`
- [x] 6.9 Записать норму о подставных собеседниках — решение владельца, работа
сверх прежнего перечня шагов. Одной строкой в документ канона: подставной
собеседник живёт в коде или в оснастке, а в боевом бинарнике появляется
только отдельным решением владельца и только под ключом, названным своим
предметом. Сегодня таких два — подмена распознавания правкой
`internal/adapter/recognizer/memory.go` и имитация заголовков конфигом, —
а записанной нормы нет вовсе, и `docs/review.md` этот пробел отмечает
вопросом темы `operations`. Дом нормы — `docs/architecture.md`, «Принципы»:
она называет, где живёт часть системы, а не как оформляют код. Запись
вопроса в `docs/review.md` правится строкой о закрытом пробеле
## 7. Приёмка
- [x] 7.1 `task gate` зелёный
- [x] 7.2 Проверить руками: сервис поднят с заполненной имитацией, приложение
открыто по адресу сервиса, второго процесса нет
- [x] 7.3 Проверить руками **записанную форму рецепта**: скопировать свежий
`config.example.toml`, применить рецепт шага 6.2 дословно и ничего сверх
него, поднять сервис, открыть приложение. Отказ старта на полпути —
поломка рецепта, а не копии
- [x] 7.4 Критерии приёмки из блока ниже проверены поимённо
## Критерии приёмки
Рубрика ревью дизайна. Проверяется поимённо шагом 7.4.
- [x] 1. **Fail-closed по умолчанию.** Отсутствие новых ключей в существующем
конфиге не меняет ни одного байта поведения; умолчание предохранителя —
«выключено»; ни одна ошибка разбора не даёт «включено». Проверяемо: конфиг
сегодняшнего дня, не тронутый, даёт ту же цепочку слоёв и те же ответы.
- [x] 2. **У каждой комбинации новых ключей назван исход, а боевая поломка
названа поимённо и не выдана за исключённую.** Машина её не исключает:
сервис с включённым предохранителем и заполненной имитацией поднимается на
любом перечне доверенных адресов, боевом в том числе, и называет пришедшего
сам всякому, чей запрос пришёл через обратный прокси. Защита названа
поимённо и целиком — умолчание предохранителя «выключено», отказ старта при
заполненной имитации без предохранителя, боевой конфиг, рендеримый шаблоном
Ansible, — и ни один документ не утверждает, что этого набора хватает на
механическую несовместимость.
- [x] 3. **Подстановка заменяет, а не дополняет.** Слой либо владеет всем
набором заголовков входа целиком, либо не трогает запрос; второго значения
того же заголовка он не создаёт никогда. Проверяемо: запрос с двумя
`Remote-User` остаётся отказом, запрос с чужим `Remote-Email` и без
`Remote-User` не собирает личность из двух источников.
- [x] 4. **Барьер доверенного адреса подставленный заголовок проходит наравне с
пришедшим, и предикат доверия у обоих один дом.** Проверяемо: запрос с
недоверенного адреса не узнаётся при включённом предохранителе так же, как
при выключенном; отбор адреса берётся из той же точки, что и у узнавания,
а не пишется вторым списком.
- [x] 5. **Отлаживается боевая ветка.** Узнавание не получает ни второго
источника значений, ни ветки «если отладка»; к моменту чтения заголовок
неотличим от поставленного прокси. Проверяемо: файл узнавания изменением
не тронут.
- [x] 6. **Область слоя равна области узнавания и выводится из неё, а не
перечисляется вторым списком.** Проверяемо: проба здоровья, метрики и
раздача приложения отвечают одинаково при включённом и выключенном
предохранителе.
- [x] 7. **Матрица отказов старта полна и тотальна: у каждой комбинации новых
ключей назван исход, и «поднялся, но никого не узнаёт» среди исходов
нет.** Отказ идёт до приёма трафика, называет имя ключа и не называет
значения. Проверяемо перечислением: предохранитель × (пустая / заполненная
/ заполненная негодно) имитация, и отдельной строкой — два ключа секции,
дающие одно каноническое имя заголовка: `Remote-User` и `remote-user`
различимы для TOML и неразличимы для HTTP, и одно из значений терялось бы
молча.
- [x] 8. **Настройка, при которой узнавание не может состояться ни при каком
запросе, — отказ старта, а не запуск.** Имитация без ключа логина, пустое,
пробельное, слишком длинное значение и значение с управляющими знаками
судятся на старте тем же правилом, каким узнавание судит пришедшее
значение. Проверяемо: конфиг с логином в 300 знаков даёт отказ старта с
именем ключа, а не сервис, отвечающий отказом на всё.
- [x] 9. **Отладочный запуск громкий и различимый.** Одна строка при старте (без
значений) и различимость «узнан подстановкой» от «узнан прокси» на уровне
запроса. Отдельно — путь, на котором подстановка не сработала: у него
обязан быть след, иначе самый частый локальный отказ (браузер пришёл
`::1`, конфиг называет `127.0.0.1`) не отличим от поломки узнавания.
- [x] 10. **Слой без состояния между запросами: исход запроса — функция конфига
и самого запроса.** Ничего не кэширует, ничего не заводит, в контекст не
пишет; заведение учётной записи остаётся там, где было, и параллельные
первые запросы с одним логином не зависят от порядка. Проверяемо: два
одновременных первых запроса дают одну учётную запись, а не две и не
отказ.
- [x] 11. **Предохранитель — закрытый перечень следствий, а не режим.** Названо,
чего он не включает; новое следствие требует отдельной нормы. Проверяемо:
включённый предохранитель при пустой имитации ведёт себя ровно как
выключенный, включая ограничитель частоты и уровень журнала.
- [x] 12. **Записанный рецепт и объявленная граница совпадают с задуманным.**
Форма, записанная в образце конфига и в памятке, — та самая, которую
проверяют; ни один документ канона не продолжает утверждать отсутствие
механизма, который изменение заводит. Проверяемо: свежая копия
`config.example.toml` доводится до рабочего локального входа ровно теми
правками, что записаны, и не даёт отказа старта на полпути.