документы: сведены расхождения, найденные сверкой канона
- один факт — один дом: рецепт локального входа, правило чтения X-Forwarded-For, уровень строки журнала и опись опор изъятия сведены к своим домам, копии заменены ссылками - форма [auth.test_headers] выровнена по образцу конфига в семи местах; сценарии intake и archive перестали ссылаться на сессию, которой сервис не выдаёт - поправлены протухшие факты: ключ объекта строит ULID, а не UUID; сверку адреса пира зовут трое, а не двое; обзор capability access знает о задаче 2026-08-23
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC
|
||||
|
||||
- **Дата:** 2026-08-22
|
||||
- **Источник:** openspec/changes/archive/trusted-header-login/design.md
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-22-trusted-header-login/design.md](../../openspec/changes/archive/2026-08-22-trusted-header-login/design.md), разделы Р1 и Р3
|
||||
|
||||
## Решение
|
||||
|
||||
|
||||
+17
-6
@@ -59,7 +59,13 @@
|
||||
`trusted-header-login` — вместе с куками, сессией и её сроком. Здесь же разграничение записей по владельцу: принятая запись
|
||||
принадлежит тому, кто её принёс, чужая неотличима от несуществующей, а ничьей
|
||||
записи не бывает вовсе — колонка владельца пустого значения не принимает.
|
||||
Задачи `record-ownership` и `remove-telegram-intake` 2026-08-14.
|
||||
Задачи `record-ownership` и `remove-telegram-intake` 2026-08-14. Здесь же
|
||||
изъятие отладочного запуска: при включённом предохранителе `[server] debug`
|
||||
заголовки входа подставляет сам сервис значениями из `[auth.test_headers]` — с
|
||||
отказами старта, строкой журнала и закрытым перечнем следствий ключа. Задача
|
||||
`config-test-headers-login` 2026-08-23; решения —
|
||||
[ADR-2026-08-23-test-headers-substituted-by-service](adr/ADR-2026-08-23-test-headers-substituted-by-service.md)
|
||||
и [ADR-2026-08-23-no-address-guard-for-debug-login](adr/ADR-2026-08-23-no-address-guard-for-debug-login.md).
|
||||
|
||||
Поведение узла, которого нет в перечне выше, по-прежнему живёт только в коде.
|
||||
Задача, которая его трогает, дописывает спеку своей capability.
|
||||
@@ -84,7 +90,7 @@
|
||||
- **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему —
|
||||
код или оснастка; в боевом бинарнике он появляется только отдельным решением
|
||||
владельца и только под ключом, названным своим предметом: имитацию заголовков
|
||||
входа объявляет секция `[auth] test_headers`, подмену распознавания — правка
|
||||
входа объявляет секция `[auth.test_headers]`, подмену распознавания — правка
|
||||
кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом,
|
||||
обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт
|
||||
себя как в бою». Предохранитель `[server] debug` вторым именем собеседнику при
|
||||
@@ -161,7 +167,7 @@
|
||||
|
||||
| Компонент | Где | Что делает |
|
||||
| --- | --- | --- |
|
||||
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, подстановка заголовков входа отладочного запуска, узнавание, требование учётной записи. Подстановка — звено необязательное: при выключенном предохранителе `[server] debug` и при пустой имитации она в цепочку не встаёт вовсе |
|
||||
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, подстановка заголовков входа отладочного запуска, узнавание, требование учётной записи. Условия, при которых звено подстановки встаёт в цепочку, нормирует [access](../openspec/specs/access/spec.md), «Отладочный запуск называет пришедшего настройками» |
|
||||
| Воркеры | `internal/controller/worker` | Пул одинаковых потоков: каждый берёт любую пригодную запись и опрашивает базу. Число — настройкой, ноль законен |
|
||||
| Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи |
|
||||
| Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности |
|
||||
@@ -184,7 +190,8 @@
|
||||
## Внешние границы и форматы
|
||||
|
||||
- **Yandex Object Storage.** S3-совместимый, клиент `aws-sdk-go-v2` с
|
||||
`UsePathStyle`. Ключ объекта — имя файла, то есть UUID с расширением.
|
||||
`UsePathStyle`. Ключ объекта — имя файла записи, то есть её идентификатор с
|
||||
расширением; идентификаторы строит `internal/ident` и они ULID, а не UUID.
|
||||
- **Yandex SpeechKit v3.** gRPC, `stt.api.cloud.yandex.net:443`, модель
|
||||
`deferred-general`, авторизация заголовком `Api-Key`. Распознавание
|
||||
асинхронное: запрос возвращает идентификатор операции, готовность опрашивается
|
||||
@@ -192,6 +199,10 @@
|
||||
- **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`.
|
||||
- **SQLite через `modernc.org/sqlite`.** Драйвер на чистом Go: CGO сборке не
|
||||
нужен. База и файлы записей лежат под одним каталогом данных.
|
||||
- **`github.com/BurntSushi/toml`.** Формат единственного источника настроек.
|
||||
Негодный TOML останавливает старт; незнакомый ключ разбор не судит и молча
|
||||
отбрасывает — наблюдение и чем оно проверено, в
|
||||
[research/toml-unknown-keys.md](research/toml-unknown-keys.md).
|
||||
- **`github.com/pressly/goose/v3`.** Шаги схемы — библиотекой, а не командной
|
||||
строкой: перечень шагов приходит провайдеру доводом, накат идёт при старте.
|
||||
Исключающей блокировки под SQLite библиотека не даёт, и замок каталога данных
|
||||
@@ -284,8 +295,8 @@
|
||||
| Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути, уровень журнала и область действия узнавания |
|
||||
| Узнавание предъявителя | `sqlite.UserRepository.EnsureUser` — поиск учётной записи по логину у провайдера и заведение при первом обращении. Дом правила один и лежит в хранилище, а не в транспорте: второй способ представиться (личные токены) возьмёт этот же метод, а уложенное куском в слой оно разошлось бы двумя копиями. Транспорт читает заголовок, судит адрес пира и зовёт метод интерфейсом `contract.UserRepository` — `internal/controller/http.TrustedHeaderIdentity` |
|
||||
| Приём значения заголовка | `internal/entity.AcceptProviderLogin`, `AcceptDisplayName`, `AcceptEmail` — правило одно на все способы представиться |
|
||||
| Имена заголовков входа | `internal/controller/http.IdentityHeaderNames` вместе с константами рядом — тройка `Remote-*` перечисляется отсюда, а не по месту. она же порождает набор имён, принимаемых секцией `[auth] test_headers`: ключ, не совпавший ни с одним, роняет старт |
|
||||
| Сверка адреса пира с перечнем доверенных | `internal/controller/http`, `identity.go` — `peerAddress` и `isTrusted`. Зовут их двое: узнавание и подстановка заголовков отладочного запуска. Второй сверщик разошёлся бы с первым молча — разбор разворачивает IPv4 в оболочке IPv6, и разница пришлась бы ровно на те адреса, ради которых он заводится |
|
||||
| Имена заголовков входа | `internal/controller/http.IdentityHeaderNames` вместе с константами рядом — тройка `Remote-*` перечисляется отсюда, а не по месту. она же порождает набор имён, принимаемых секцией `[auth.test_headers]`; что делает старт с ключом вне набора, нормирует [access](../openspec/specs/access/spec.md), «Настройка, открывающая вход всем, роняет старт» |
|
||||
| Сверка адреса пира с перечнем доверенных | `internal/controller/http`, `identity.go` — `peerAddress` и `isTrusted`. Зовут их узнавание, подстановка заголовков отладочного запуска и ограничитель частоты. Свой сверщик разошёлся бы с общим молча — разбор разворачивает IPv4 в оболочке IPv6, и разница пришлась бы ровно на те адреса, ради которых он заводится |
|
||||
| Ограничитель частоты | `internal/controller/http.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса |
|
||||
|
||||
Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось.
|
||||
|
||||
@@ -63,12 +63,10 @@ force_shutdown_timeout = <N> # ждать остановки ворке
|
||||
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
|
||||
входом.
|
||||
|
||||
Там же, комментарием под секцией, стоит **рецепт локального входа одним связным
|
||||
блоком**: три правки сверху вниз — пара петлевых адресов в перечень доверенных,
|
||||
`[server] debug = true`, раскомментированная секция `[auth.test_headers]` с
|
||||
ключом `Remote-User`. Блок один, а не три комментария по месту: правки связаны
|
||||
между собой, и применённая порознь любая из них роняет старт либо оставляет
|
||||
сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение —
|
||||
Там же, комментарием под секцией, стоит рецепт локального входа **одним связным
|
||||
блоком**, а не тремя комментариями по месту: правки связаны между собой, и
|
||||
применённая порознь любая из них роняет старт либо оставляет сервис никого не
|
||||
узнающим. Рабочей строкой в образце стоит боевое значение —
|
||||
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
|
||||
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
|
||||
дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
|
||||
|
||||
@@ -87,10 +87,10 @@ stdlib-логом в поток ошибок. Это выбор, а не дол
|
||||
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
||||
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
||||
|
||||
*Изъятие:* строка о подставленных заголовках входа
|
||||
(`internal/controller/http/substitute.go`) адресована разработчику, а идёт на
|
||||
`INFO`: `DEBUG` включить нечем, а в бою она не пишется вовсе — подстановку
|
||||
держит выключенный умолчанием предохранитель `[server] debug`.
|
||||
*Изъятие:* строка о подставленных заголовках входа адресована разработчику, а
|
||||
идёт на `INFO` — уровень и его довод нормирует спека
|
||||
[access](../../openspec/specs/access/spec.md), «Отладочный запуск виден в
|
||||
журнале».
|
||||
|
||||
## Время
|
||||
|
||||
|
||||
+2
-3
@@ -424,9 +424,8 @@ Crockford. Выдаёт их приложение единой точкой `int
|
||||
и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и
|
||||
восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка —
|
||||
верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он
|
||||
ограничивает. Сама цепочка читается справа налево с отбрасыванием доверенных
|
||||
адресов, поэтому дописывающий прокси правилом покрыт; почему так —
|
||||
[security.md](security.md), «Периметр».
|
||||
ограничивает. Как читается цепочка — спека
|
||||
[archive](../openspec/specs/archive/spec.md); здесь только числа бюджета.
|
||||
|
||||
Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа
|
||||
провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ
|
||||
|
||||
+1
-1
@@ -345,7 +345,7 @@ API и имя не откатываются обратной правкой по
|
||||
|
||||
**Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде
|
||||
сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по
|
||||
секции `[auth] test_headers` под предохранителем `[server] debug` — прежде
|
||||
секции `[auth.test_headers]` под предохранителем `[server] debug` — прежде
|
||||
`cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание,
|
||||
заведение учётной записи первым обращением, отказ с недоверенного адреса и
|
||||
отказ старта на пустом перечне.
|
||||
|
||||
+12
-13
@@ -85,10 +85,12 @@ Telegram — связи чата с учётной записью сервис
|
||||
**Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23**
|
||||
задачей `config-test-headers-login`. При включённом предохранителе
|
||||
`[server] debug` заголовки входа ставит не прокси, а сам сервис значениями из
|
||||
секции `[auth] test_headers`: на машине разработчика прокси нет, а браузер
|
||||
секции `[auth.test_headers]`: на машине разработчика прокси нет, а браузер
|
||||
заголовков не ставит. Узнавание при этом остаётся тем же и подставленного
|
||||
заголовка от пришедшего не отличает — отлаживается боевая ветка. Нормирует
|
||||
изъятие спека [access](../openspec/specs/access/spec.md).
|
||||
изъятие спека [access](../openspec/specs/access/spec.md), решение о подстановке
|
||||
самим сервисом —
|
||||
[ADR-2026-08-23-test-headers-substituted-by-service](adr/ADR-2026-08-23-test-headers-substituted-by-service.md).
|
||||
|
||||
Держится оно тремя вещами, и других нет: умолчание предохранителя —
|
||||
«выключено»; заполненная имитация при выключенном предохранителе роняет старт с
|
||||
@@ -103,19 +105,16 @@ Telegram — связи чата с учётной записью сервис
|
||||
перечне доверенных адресов и назовёт своим именем всякого, чей запрос пришёл
|
||||
через обратный прокси, — то есть всякого, кто пришёл обычным путём. Адресного
|
||||
предохранителя у изъятия нет: требование петлевого перечня рассматривалось и
|
||||
снято решением владельца на чекпоинте задачи.
|
||||
снято — [ADR-2026-08-23-no-address-guard-for-debug-login](adr/ADR-2026-08-23-no-address-guard-for-debug-login.md).
|
||||
|
||||
**`X-Forwarded-For` сервис читает сам, и правило чтения закрывает дописывание.**
|
||||
С 2026-08-22 адрес спрашивающего ограничитель частоты берёт из этого заголовка:
|
||||
иначе счётчик ведётся по адресу пира, а пир теперь всегда один — прокси, — и
|
||||
бюджет становится общим на весь сервис. Цепочка читается **справа налево**,
|
||||
доверенные адреса отбрасываются, и ключом становится первый недоверенный: левым
|
||||
значением распоряжается сам спрашивающий, а правое приписал ближайший к нам
|
||||
прокси. Заголовок читается всеми строками, а не одной: цепочка законно приходит
|
||||
несколькими. Прокси, дописывающий `X-Forwarded-For` к присланному, этим правилом
|
||||
покрыт, и требования «перезаписывать, а не дописывать» у сервиса к нему нет — в
|
||||
отличие от `Remote-*`. Барьером узнавания заголовок при этом не служит: кто
|
||||
пришёл, решает адрес самого соединения.
|
||||
Как именно читается цепочка, нормирует спека
|
||||
[archive](../openspec/specs/archive/spec.md), «Адреса приложения живут своим
|
||||
пространством». Отсюда периметровое следствие: прокси, дописывающий
|
||||
`X-Forwarded-For` к присланному, этим правилом покрыт, и требования
|
||||
«перезаписывать, а не дописывать» у сервиса к нему нет — в отличие от `Remote-*`.
|
||||
Барьером узнавания заголовок при этом не служит: кто пришёл, решает адрес самого
|
||||
соединения.
|
||||
|
||||
**Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.**
|
||||
Перечень задаёт, чьему `Remote-User` верить, и всякий, кто дотянулся до сервиса
|
||||
|
||||
Reference in New Issue
Block a user