локальный вход задаётся конфигом: заголовки подставляет сам сервис
- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# Адресного предохранителя у отладочного входа нет: держит его умолчание, а не машина
|
||||
|
||||
- **Дата:** 2026-08-23
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-23-config-test-headers-login/design.md](../../openspec/changes/archive/2026-08-23-config-test-headers-login/design.md),
|
||||
решение 4
|
||||
|
||||
## Решение
|
||||
|
||||
Отладочная подстановка заголовков входа не требует от настроек ничего сверх
|
||||
самого предохранителя `[server] debug`. Решение владельца на чекпоинте записано
|
||||
в источнике дословно:
|
||||
|
||||
> «предохранитель по адресам не делаем, полагаемся только на параметр debug».
|
||||
|
||||
Рассматривалось требование, чтобы при включённом предохранителе перечень
|
||||
доверенных адресов состоял только из петлевых записей; оно снято вместе с
|
||||
предикатом «петлевая запись», который заводился ровно ради него.
|
||||
|
||||
Согласованность с барьером узнавания при этом остаётся: подставленный заголовок
|
||||
проходит тот же перечень доверенных адресов, что и пришедший, и судит адрес та
|
||||
же функция. Предохранителем это не служит — «от конфига она не требует ничего и
|
||||
круга тех, кто мог назваться кем угодно, не расширяет».
|
||||
|
||||
## Почему
|
||||
|
||||
Решение покупает работоспособность отладочного входа там, где адрес пира не
|
||||
петлевой:
|
||||
|
||||
> отладочный вход работает **внутри контейнера** — адрес пира там принадлежит
|
||||
> сети докера, и она же стоит в боевом перечне, — а локальный прогон не
|
||||
> переставляет перечень доверенных адресов на петлевой: петлевые записи
|
||||
> добавляются к тем, что в нём уже стоят.
|
||||
|
||||
Цена названа в источнике прямо, и владелец принял именно её:
|
||||
|
||||
> Что этим потеряно, и это надо назвать прямо: боевую поломку больше не ловит
|
||||
> машина. Сервис, поднятый в бою с включённым предохранителем и заполненной
|
||||
> имитацией, отдаст архив всякому, кто дотянулся до него с доверенного адреса, —
|
||||
> а доверенный адрес в бою это адрес обратного прокси, то есть **любой запрос,
|
||||
> пришедший обычным путём**.
|
||||
|
||||
Между боевой выкладкой и открытым входом остаётся три вещи, и других нет:
|
||||
умолчание предохранителя «выключено»; отказ старта при заполненной имитации без
|
||||
предохранителя; боевой конфиг, который рендерит шаблон Ansible, а не
|
||||
копируют с машины разработчика.
|
||||
|
||||
Отвергнуты вместе с адресным предохранителем ещё два подхода. **Принудительно
|
||||
слушать петлевой адрес при включённом предохранителе** — «меняет поведение молча
|
||||
… и закрывает ровно то, что решение покупает: внутри контейнера сервис слушает не
|
||||
петлю». **Новый ключ `[server] listen`** — «публичная поверхность настроек ради
|
||||
предохранителя, которого решением владельца нет».
|
||||
|
||||
## Почему это ADR
|
||||
|
||||
Триггер — **намеренный отказ** от очевидного подхода. Требовать петлевой перечень
|
||||
при включённом отладочном входе — первое, что предлагает всякий, кто читает
|
||||
модель угроз; отказ от этого оставляет боевую поломку, которую машина не
|
||||
исключает, и объяснить его надо один раз здесь, а не на каждом ревью, которое
|
||||
эту дыру находит заново.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` Отладочный вход работает и на машине разработчика, и внутри контейнера:
|
||||
перечень доверенных адресов остаётся границей доверия, а не признаком отладки.
|
||||
- `+` Локальный прогон не переставляет перечень на петлевой — петлевые записи к
|
||||
нему добавляются.
|
||||
- `+` Предиката «петлевая запись» в коде нет вовсе: он заводился ради одной этой
|
||||
проверки.
|
||||
- `−` **Машина не исключает боевую поломку «конфиг с `debug = true` и
|
||||
заполненной имитацией».** Такой сервис поднимется на любом перечне доверенных
|
||||
адресов и назовёт своим именем всякого, кто пришёл обычным путём. Записано это в модели
|
||||
угроз, [security.md](../security.md), «Периметр», и в спеке
|
||||
[access](../../openspec/specs/access/spec.md).
|
||||
- `−` Одна из трёх опор лежит вне репозитория: шаблон Ansible из
|
||||
`pet-project-server`. Проверить её отсюда нечем — тем же свойством обладает
|
||||
правило прокси про заголовки `Remote-*`.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Заголовки входа отладочного запуска подставляет сам сервис, а не второй процесс
|
||||
|
||||
- **Дата:** 2026-08-23
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-23-config-test-headers-login/design.md](../../openspec/changes/archive/2026-08-23-config-test-headers-login/design.md),
|
||||
решения 1, 6, 7 и 10
|
||||
|
||||
## Решение
|
||||
|
||||
Заголовки входа на машине разработчика ставит сам сервис — отдельным слоем
|
||||
цепочки корня приложения, а не вспомогательным процессом рядом:
|
||||
|
||||
> отдельный слой цепочки корня приложения, стоящий **перед**
|
||||
> `TrustedHeaderIdentity` и **после** ограничителя частоты. Он правит заголовки
|
||||
> запроса и ничего больше не делает: учётной записи не заводит, отказов не
|
||||
> выдаёт, в контекст не пишет.
|
||||
|
||||
Включают слой два новых ключа настроек — предохранитель `[server] debug` и
|
||||
секция значений `[auth.test_headers]`. Прежний вспомогательный процесс уходит:
|
||||
|
||||
> **Решено** владельцем на чекпоинте: подкоманда удаляется. Назначения у неё не
|
||||
> остаётся — всё, ради чего её поднимали, делает сам сервис, — и второго способа
|
||||
> входить локально не остаётся тоже.
|
||||
|
||||
Имена заголовков служат именами ключей секции, но набор принимаемых имён
|
||||
порождают константы транспорта: дом у имён остаётся один, а ключ, не совпавший
|
||||
ни с одним из них, роняет старт.
|
||||
|
||||
## Почему
|
||||
|
||||
Владелец назвал желаемое: один бинарник, различия между запусками — в конфиге.
|
||||
Дизайн записал это целью:
|
||||
|
||||
> Локальный запуск идёт одним процессом и одной командой; **вход** — то, кем
|
||||
> назвался пришедший, — отличает тестовый прогон от боевого содержимым файла
|
||||
> настроек, и ничем больше.
|
||||
|
||||
Довод в пользу слоя перед узнаванием, а не внутри него:
|
||||
|
||||
> Отлаживается **та же** ветка кода, что работает в бою: подставленный заголовок
|
||||
> неотличим от пришедшего от Caddy к моменту, когда его читает узнавание.
|
||||
|
||||
Отвергнуты три очевидных подхода, и у каждого названа цена. **Подстановка внутри
|
||||
`TrustedHeaderIdentity`** — «узнавание получило бы второй источник значений и
|
||||
ветку, которой в бою нет. Отлаживалась бы не боевая ветка, а её отладочный
|
||||
двойник». **Произвольная карта имён заголовков в конфиге** — «опечатка
|
||||
`Remote-Usr` даёт „сервис меня не узнаёт“ без единого следа». **Вырезать
|
||||
подстановку из боевой сборки тегом сборки:**
|
||||
|
||||
> сборка образа в гейте не проверяется вовсе (`CLAUDE.md`, «Гейт»), и тег,
|
||||
> забытый в одной ступени, дал бы ровно ту тишину, которой избегает пункт 3.
|
||||
|
||||
## Почему это ADR
|
||||
|
||||
Триггер сработал дважды. **Дорогой откат:** решение заводит два имени ключа
|
||||
настроек, а имя ключа конфига `CLAUDE.md` называет необратимым; вернуться к
|
||||
вспомогательному процессу значит поднять удалённую подкоманду, убрать оба ключа
|
||||
из настроек и переписать рецепт локального запуска, разошедшийся по образцу
|
||||
конфига, `README.md`, `CLAUDE.md` и конвенции настроек. **Намеренный отказ:**
|
||||
вырезать отладочный код из боевой сборки тегом сборки — то, что делают по
|
||||
умолчанию, и отказ от этого объясняется один раз здесь, а не на каждом вопросе
|
||||
«почему подстановка вообще есть в боевом бинарнике».
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` Локальный запуск идёт одним процессом и одной командой; приложение
|
||||
открывают по адресу сервиса, второго порта нет.
|
||||
- `+` Отлаживается боевая ветка узнавания: подставленный заголовок неотличим от
|
||||
пришедшего от прокси к моменту, когда его читают.
|
||||
- `+` Второго способа входить локально не остаётся, и документация перестаёт
|
||||
каждый раз говорить, какой из способов чей.
|
||||
- `+` Имена заголовков остаются с одним домом — константами транспорта; ключ, не
|
||||
совпавший ни с одним из них, роняет старт и называет принимаемые имена.
|
||||
- `−` **Местный инструмент больше не воспроизводит поломки контура.** Цена
|
||||
названа в источнике прямо: два значения `Remote-User`, заголовок с
|
||||
недоверенного адреса, цепочка `X-Forwarded-For` — всё это теперь
|
||||
воспроизводит только автотест, ставящий заголовок сам.
|
||||
- `−` В боевом бинарнике появляется код, называющий пришедшего без провайдера.
|
||||
Что его держит и чего у него нет — [ADR-2026-08-23-no-address-guard-for-debug-login](ADR-2026-08-23-no-address-guard-for-debug-login.md).
|
||||
- `−` У ключа `[server] debug` закрытый перечень следствий, и держать его
|
||||
придётся руками: новое поведение привязывается к ключу только отдельным
|
||||
решением владельца и получает своё требование спеки
|
||||
[access](../../openspec/specs/access/spec.md). Ключ с открытым перечнем
|
||||
следствий обрастает ими молча.
|
||||
- `−` Каждый новый логин имитации заводит учётную запись, а удалять их сервис не
|
||||
умеет. Локальная база ронится и пересоздаётся свободно, в бою подстановка
|
||||
выключена — но лишние записи копятся.
|
||||
@@ -35,6 +35,8 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-23 | [Адресного предохранителя у отладочного входа нет: держит его умолчание, а не машина](ADR-2026-08-23-no-address-guard-for-debug-login.md) | |
|
||||
| 2026-08-23 | [Заголовки входа отладочного запуска подставляет сам сервис, а не второй процесс](ADR-2026-08-23-test-headers-substituted-by-service.md) | |
|
||||
| 2026-08-22 | [Хранилищем становится SQLite с каталогом файлов, а PocketBase уходит целиком](ADR-2026-08-22-storage-without-pocketbase.md) | |
|
||||
| 2026-08-22 | [Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC](ADR-2026-08-22-login-by-trusted-header.md) | |
|
||||
| 2026-08-15 | [Node зовётся контейнером, а не ставится на машину разработчика](ADR-2026-08-15-node-in-container-not-on-machine.md) | |
|
||||
|
||||
+20
-3
@@ -81,6 +81,18 @@
|
||||
задача возвращается в работу, нормирует
|
||||
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||
- **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему —
|
||||
код или оснастка; в боевом бинарнике он появляется только отдельным решением
|
||||
владельца и только под ключом, названным своим предметом: имитацию заголовков
|
||||
входа объявляет секция `[auth] test_headers`, подмену распознавания — правка
|
||||
кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом,
|
||||
обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт
|
||||
себя как в бою». Предохранитель `[server] debug` вторым именем собеседнику при
|
||||
этом не служит и правилу не противоречит: собой он не называет ничего, а держит
|
||||
**закрытый** перечень следствий, и перечень этот ведёт спека
|
||||
[access](../openspec/specs/access/spec.md), «Предохранитель отладки включает
|
||||
только подстановку заголовков». Новое следствие вешается на ключ только новым
|
||||
требованием той же спеки.
|
||||
- **Чистая архитектура.** Зависимости направлены внутрь, к домену: внутренний
|
||||
слой не знает внешнего никогда. `internal/service` знает только
|
||||
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в точке входа
|
||||
@@ -149,7 +161,7 @@
|
||||
|
||||
| Компонент | Где | Что делает |
|
||||
| --- | --- | --- |
|
||||
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, узнавание, требование учётной записи |
|
||||
| HTTP API | `internal/controller/http` | Адреса приложения под корнем `/app/` на `net/http`: приём записи, страница своих записей, карточка, текст названного вида, файл записи, пределы сервера и «кто вошёл». Слои — свои: журнал, восстановление после паники, ограничитель частоты, подстановка заголовков входа отладочного запуска, узнавание, требование учётной записи. Подстановка — звено необязательное: при выключенном предохранителе `[server] debug` и при пустой имитации она в цепочку не встаёт вовсе |
|
||||
| Воркеры | `internal/controller/worker` | Пул одинаковых потоков: каждый берёт любую пригодную запись и опрашивает базу. Число — настройкой, ноль законен |
|
||||
| Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи |
|
||||
| Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности |
|
||||
@@ -157,7 +169,7 @@
|
||||
| Репозитории | `internal/adapter/repo/sqlite` | Учётные записи, записи, файлы, тексты, структура, попытки распознавания и журнал событий — таблицами базы; захват — одним запросом с `RETURNING` по пишущему соединению |
|
||||
| Файлы записей | `internal/adapter/repo/sqlite`, `store.go` | Подкаталог на запись под её идентификатором; укладка атомарна — временное имя рядом и переименование |
|
||||
| Шаги схемы | `internal/adapter/repo/sqlite/migrations` | Файл на шаг, версия — число в начале имени; накатывает `pressly/goose/v3` под своим замком |
|
||||
| Оснастка владельца | `cmd/devtools` | Подставной прокси для местного запуска и возврат остановленной записи в работу. Панели у сервиса нет и не будет: экраны правки приносят отдельные задачи |
|
||||
| Оснастка владельца | `cmd/devtools` | Возврат остановленной записи в работу. Панели у сервиса нет и не будет: экраны правки приносят отдельные задачи |
|
||||
| Приложение | `web/` | Vue 3, роутер пятой версии, сборка Vite. Собранное лежит в `web/embed/dist` и вшивается в бинарник; в git его нет |
|
||||
| Раздача приложения | `internal/controller/http`, `webapp.go` | Корневой маршрут: разметка вне корней сервиса, отказ внутри, срок хранения по каталогу сборщика |
|
||||
|
||||
@@ -200,7 +212,10 @@
|
||||
целиком. Оставшиеся ключи, которых новый образ ждёт, в конфиге уже есть.
|
||||
Секцию `[telegram]` и ключ `server.users_while_list` человек убирает из боевого
|
||||
файла после выкладки: незнакомые ключи разбор настроек не судит, и файл с ними
|
||||
сервис поднимает молча.
|
||||
сервис поднимает молча. Чем это обеспечено и как проверено —
|
||||
[research/toml-unknown-keys.md](research/toml-unknown-keys.md); тем же
|
||||
свойством безопасно и обратное направление: прежний образ поднимается на
|
||||
конфиге с ключами, которых он ещё не знает.
|
||||
- **Откат образа на версию до 2026-08-22 не работает вовсе.** Каталог данных
|
||||
сменил раскладку целиком: база зовётся другим файлом, файлы записей лежат
|
||||
другими путями, а учёт применённых шагов ведёт другая таблица. Прежний образ на
|
||||
@@ -269,6 +284,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.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса |
|
||||
|
||||
Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось.
|
||||
|
||||
@@ -63,12 +63,23 @@ force_shutdown_timeout = <N> # ждать остановки ворке
|
||||
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
|
||||
входом.
|
||||
|
||||
Там же, комментарием под секцией, стоит **второе значение перечня — петлевой
|
||||
адрес, под подставной прокси `cmd/devtools proxy`**. Оно стоит закомментированным, и это
|
||||
намеренно: образец описывает боевую выкладку, а локальный вход — способ до неё
|
||||
Там же, комментарием под секцией, стоит **рецепт локального входа одним связным
|
||||
блоком**: три правки сверху вниз — пара петлевых адресов в перечень доверенных,
|
||||
`[server] debug = true`, раскомментированная секция `[auth.test_headers]` с
|
||||
ключом `Remote-User`. Блок один, а не три комментария по месту: правки связаны
|
||||
между собой, и применённая порознь любая из них роняет старт либо оставляет
|
||||
сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение —
|
||||
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
|
||||
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
|
||||
дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
|
||||
какое из них чьё.
|
||||
|
||||
*Расхождение:* петлевые адреса в рецепте названы **парой** — `127.0.0.1` и
|
||||
`::1`, — а не одним значением, хотя правило секции требует от образца только
|
||||
формы значения. Причина в цене: браузер разрешает `localhost` в IPv6 не реже,
|
||||
чем в IPv4, и перечень без `::1` даёт неузнанный запрос там, где человек ждёт
|
||||
входа.
|
||||
|
||||
## Поля по дискриминатору `type`
|
||||
|
||||
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из
|
||||
@@ -137,6 +148,16 @@ TOML. Пустые ключи Yandex ловятся в конструкторе
|
||||
Два ключа секции `[telegram]`, стоявшие здесь исключением, ушли вместе с самим
|
||||
входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.
|
||||
|
||||
**Проверка, охватывающая две секции разом, живёт методом на корневой `Config`.**
|
||||
Такая сегодня одна — `ValidateTestHeaders`: она судит `[server] debug` против
|
||||
`[auth.test_headers]`, и ни в `Validate()` секции сервера, ни в `Validate()`
|
||||
секции входа не помещается — секция начала бы знать о чужой секции. Зовётся она
|
||||
из `cmd/transcriber` рядом с остальными. Имена принимаемых заголовков приходят
|
||||
ей **доводом**, а не читаются из пакета настроек: дом у них один — константы
|
||||
транспорта, — а `internal/config` транспорта не знает и знать не должен, иначе
|
||||
`cmd/devtools`, которому нужен один разбор конфига, линковал бы всю поверхность
|
||||
HTTP.
|
||||
|
||||
Секции `[auth]`, `[pipeline]` и `[storage]` проверяют себя сами, и проверка стоит
|
||||
на старте: `Validate()` каждой зовётся из `cmd/transcriber` сразу после загрузки
|
||||
и роняет процесс с именем незаполненного ключа. У `[storage]` это ожидание занятой
|
||||
|
||||
@@ -173,3 +173,10 @@ transcriber — **приложение, а не библиотека**: внеш
|
||||
|
||||
- Сбор независимых ошибок (проверка конфига — все проблемы разом) —
|
||||
`errors.Join`; проверка собранного по-прежнему через `errors.Is`.
|
||||
|
||||
*Расхождение:* проверку конфига пункт называет поимённо, а ни одна из них так не
|
||||
устроена: `errors.Join` в `internal/config` не зовётся нигде, и всякая проверка
|
||||
возвращается на первом несовпадении. Заметило ревью задачи
|
||||
`config-test-headers-login` 2026-08-23 — тем же прогоном, каким добавили
|
||||
`ValidateTestHeaders`, ведущую себя так же. Человек, заполняющий конфиг
|
||||
впервые, чинит одну ошибку за прогон.
|
||||
|
||||
@@ -87,6 +87,11 @@ stdlib-логом в поток ошибок. Это выбор, а не дол
|
||||
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
||||
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
||||
|
||||
*Изъятие:* строка о подставленных заголовках входа
|
||||
(`internal/controller/http/substitute.go`) адресована разработчику, а идёт на
|
||||
`INFO`: `DEBUG` включить нечем, а в бою она не пишется вовсе — подстановку
|
||||
держит выключенный умолчанием предохранитель `[server] debug`.
|
||||
|
||||
## Время
|
||||
|
||||
- Поле — `time` (ключ `slog` по умолчанию).
|
||||
|
||||
+10
-3
@@ -65,9 +65,16 @@ Telegram.
|
||||
записи у сервиса при этом есть, и границы это не двигает: сервис **зеркалит**
|
||||
имя, названное провайдером, — заводит строку при первом обращении под новым
|
||||
именем и связывает с ней записи владельца. Кто этот человек и пускать ли его,
|
||||
сервис не решает никогда. Исключений у этого больше нет: панель администратора
|
||||
со своим паролем владельца жила здесь с 2026-08-11 по 2026-08-22 и ушла вместе
|
||||
со встроенным хранилищем — своего входа сервис не ведёт вовсе.
|
||||
сервис не решает никогда. Панель администратора со своим паролем владельца жила
|
||||
здесь с 2026-08-11 по 2026-08-22 и ушла вместе со встроенным хранилищем.
|
||||
*Изъятие одно:* при включённом предохранителе `[server] debug`, выключенном по
|
||||
умолчанию, сервис подставляет запросу те заголовки входа, которые в бою даёт
|
||||
обратный прокси. Своего входа, регистрации и проверки допуска он от этого не
|
||||
заводит: подставленное имя проходит то же узнавание, что и пришедшее. Кого
|
||||
пускать, провайдер решает во всяком прогоне без изъятия; в самом изъятии его не
|
||||
спрашивают вовсе — сервис называет пришедшего сам. Тем изъятие и держится
|
||||
выключенным умолчанием, а границу его держит спека
|
||||
[access](../openspec/specs/access/spec.md).
|
||||
- **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не
|
||||
обрабатываем.
|
||||
- **Диктофон.** Запись звука делает телефон, а приложение принимает готовый
|
||||
|
||||
@@ -27,6 +27,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-23 | [Разбор TOML: незнакомый ключ и незнакомая секция не отказ, а тишина](toml-unknown-keys.md) | `err == nil` на опечатке в имени секции, потерянное только в `MetaData.Undecoded()`, безопасное направление отката в BurntSushi/toml v1.5.0 |
|
||||
| 2026-08-22 | [Хранилище: PocketBase против голого SQLite с каталогом файлов](storage-without-pocketbase.md) | Шесть ролей библиотеки в этом коде, отпавший довод перевода, объём кода на её типах, шесть модулей только через неё |
|
||||
| 2026-08-15 | [Раздача приложения: что делают за нас библиотека и сборщик](webapp-serving.md) | Раскодированный путь у маршрутизатора, второй журнал у PocketBase, нулевое время у вшитого файла, зависание установщика без сети |
|
||||
| 2026-08-13 | [Разбор TOML: какое семейство отказов несёт значения из файла](toml-decode-errors.md) | Значения только в `ParseError.Message`, врущее поле `Line`, отказ значением в BurntSushi/toml v1.5.0 |
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Разбор TOML: незнакомый ключ и незнакомая секция не отказ, а тишина
|
||||
|
||||
Отвечает на вопрос, возникший по ходу задачи `config-test-headers-login`: что
|
||||
делает декодер настроек с ключом и секцией, которых структура не знает, и виден
|
||||
ли этот случай хоть чем-нибудь. Наблюдение понадобилось потому, что ревью нашло
|
||||
опечатку в имени новой секции `[auth.test_headers]`, проходящую молча, и без
|
||||
разреза нельзя было сказать, где кончается предмет задачи и начинается свойство
|
||||
самой библиотеки.
|
||||
|
||||
Соседняя записка о той же библиотеке — [toml-decode-errors.md](toml-decode-errors.md)
|
||||
— разбирает семейства **отказов**; здесь предмет обратный: случай, отказа не
|
||||
дающий.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Прогонами на зависимости, зафиксированной в `go.mod`:
|
||||
`github.com/BurntSushi/toml` версии **v1.5.0**. Оба уровня снял триаж ревью
|
||||
2026-08-23, отчёт —
|
||||
[triage-2026-08-23.md](../../openspec/changes/archive/2026-08-23-config-test-headers-login/review/triage-2026-08-23.md),
|
||||
находка 2 и факт, подтверждённый разбором прохода `operations`. Временные файлы
|
||||
прогонов удалены, бинарник поднимался в каталог вне репозитория, не в `data/`.
|
||||
|
||||
- **Модульный.** Вход
|
||||
`[server]\ndebug = true\n[auth]\n[auth.test_headrs]\n"Remote-User" = "dev"` —
|
||||
опечатка в имени секции.
|
||||
- **Сквозной.** Настоящий бинарник на конфиге с той же опечаткой, порт 18099,
|
||||
каталог данных вне репозитория; проба `curl /app/me`.
|
||||
|
||||
## Что выяснилось
|
||||
|
||||
- **Незнакомая секция и незнакомый ключ отказа не дают: `decode err=<nil>`.**
|
||||
Разбор проходит целиком, поля структуры остаются нулевыми, и отличить «в файле
|
||||
этого нет» от «в файле это написано с опечаткой» по результату разбора нельзя.
|
||||
В прогоне: `Server.Debug=true len(TestHeaders)=0`.
|
||||
- **Потерянное называет только `MetaData.Undecoded()`.** Он возвращает перечень
|
||||
путей, которых структура не знала: `[auth.test_headrs
|
||||
auth.test_headrs.Remote-User]`. Значение это в проекте не читает никто — ни
|
||||
загрузка настроек, ни проверки старта.
|
||||
- **Контроль показывает, что дело в уровне, а не в разборе вообще.** Ту же
|
||||
опечатку **внутри** известной секции (`Remote-Usr` вместо `Remote-User`)
|
||||
ловит проверка старта — `auth: секция [auth.test_headers] называет
|
||||
заголовок, которого сервис не читает: Remote-Usr`, — потому что судит её код
|
||||
проекта, а не библиотека. Ошибка в имени самой секции до этого кода не
|
||||
доходит.
|
||||
- **Сквозной прогон следа не оставляет вовсе.** Бинарник поднимается без
|
||||
предупреждения, `curl /app/me` отвечает `401`, а в журнале стоит только
|
||||
`INFO "Incoming request" … http.status_code=401`.
|
||||
- **Отсюда направление отката бинарника безопасно.** Прежний образ, получивший
|
||||
конфиг с ключами, которых его структура ещё не знает, эти ключи игнорирует и
|
||||
поднимается. Свойство держится ровно на тишине выше: перечень
|
||||
`MetaData.Undecoded()` никто не судит.
|
||||
|
||||
## Что из этого следует для кода
|
||||
|
||||
Свойство сегодня используется, а не терпится: правило выкладки «конфиг после
|
||||
образа» опирается именно на него, и его дом — [../architecture.md](../architecture.md),
|
||||
«Эксплуатация». Здесь записано, чем свойство обеспечено и как проверено, а не
|
||||
надо ли его менять.
|
||||
|
||||
Отсюда же цена любой будущей проверки `MetaData.Undecoded()`: непонятый ключ,
|
||||
роняющий старт, закрывает опечатки во всех секциях разом — и тем же движением
|
||||
снимает безопасность отката, потому что прежний образ перестанет поднимать
|
||||
конфиг новее себя. Разменивать одно на другое — отдельное решение владельца,
|
||||
а не попутная правка.
|
||||
|
||||
**Наблюдение привязано к версии.** Версия, начавшая судить незнакомые ключи
|
||||
сама, сменит оба следствия разом — молчаливую опечатку и безопасный откат.
|
||||
+23
-9
@@ -31,6 +31,13 @@
|
||||
квота на три темы. Механизации нет — потолок объявляет сам проход, и заставить его нечем;
|
||||
остаётся сверка триажа.
|
||||
|
||||
Пробел повторился на прогоне `config-test-headers-login` 2026-08-23, и это уже
|
||||
не единичный случай: строки о потолке не дал ни один из шести проходов, а
|
||||
заметил это снова только триаж. Прогон при этом шёл с меткой `large`, то есть с
|
||||
самым широким составом, — и разница с прошлым разом ровно в числе проходов,
|
||||
промолчавших одинаково. Читать пробел надо как границу покрытия каждого прогона,
|
||||
а не как свойство одного из них.
|
||||
|
||||
Что уже проверяет машина и о чём поэтому спрашивать не нужно — конвенция
|
||||
[conventions/go-linters.md](conventions/go-linters.md). Вопросы ниже — то, чего
|
||||
машина не проверяет; свойства, которые обязан проверять тест, — в «Типовых
|
||||
@@ -178,12 +185,17 @@
|
||||
в проверках. Прецедент: подставных провайдера OIDC в репозитории было два —
|
||||
`cmd/oidcstub` и `fakeProvider` в проверках входа, — с теми же адресами и той
|
||||
же посылкой, и они уже разошлись в мелочи (`token_type` «bearer» против
|
||||
«Bearer»). Оба ушли 2026-08-22 вместе с протоколом; на их месте
|
||||
`cmd/devtools proxy`, а проверки ставят заголовок сами и подставного собеседника
|
||||
не держат вовсе. Тем же вопросом судится подставной распознаватель. Записанной
|
||||
конвенции о единственном доме подставных внешних собеседников у проекта нет,
|
||||
поэтому спрашивать надо, а не считать нарушением (ревью задачи про заглушку
|
||||
OIDC, 2026-08-15).
|
||||
«Bearer»). Оба ушли 2026-08-22 вместе с протоколом; на их месте встал
|
||||
`cmd/devtools proxy`, а 2026-08-23 задачей `config-test-headers-login` убран и
|
||||
он: заголовки входа подставляет сам сервис под предохранителем
|
||||
`[server] debug`. Проверки ставят заголовок сами и подставного собеседника не
|
||||
держат вовсе. Тем же вопросом судится подставной распознаватель. **Пробел
|
||||
закрыт той же задачей:** норма о подставных собеседниках записана в
|
||||
[architecture.md](architecture.md), «Принципы» — пункт «Подставной собеседник
|
||||
в боевом бинарнике объявлен своим ключом»; здесь она не пересказывается.
|
||||
Вопрос при этом остаётся вопросом:
|
||||
норма называет, где собеседнику жить, а не сколько домов у него уже завелось
|
||||
(ревью задачи про заглушку OIDC, 2026-08-15).
|
||||
- `operations`: как шаг отвечает на отмену посреди работы — контекст доходит до
|
||||
внешнего собеседника и это держат правила `noctx` и `contextcheck`
|
||||
([conventions/go-linters.md](conventions/go-linters.md), «Отмена и внешний
|
||||
@@ -332,9 +344,11 @@ API и имя не откатываются обратной правкой по
|
||||
остановку; нельзя — расшифровку и заливку.
|
||||
|
||||
**Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде
|
||||
сессию в прогоне выдать было нечем; теперь заголовок ставит `cmd/devtools
|
||||
proxy`, и живьём проверяются узнавание, заведение учётной записи первым
|
||||
обращением, отказ с недоверенного адреса и отказ старта на пустом перечне.
|
||||
сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по
|
||||
секции `[auth] test_headers` под предохранителем `[server] debug` — прежде
|
||||
`cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание,
|
||||
заведение учётной записи первым обращением, отказ с недоверенного адреса и
|
||||
отказ старта на пустом перечне.
|
||||
Настоящая Authelia по-прежнему недоступна — её правило на домен живёт в
|
||||
контуре (см. «Не проверит ни один проход»).
|
||||
|
||||
|
||||
+34
-9
@@ -77,6 +77,29 @@ Telegram — связи чата с учётной записью сервис
|
||||
`Remote-*` прокси обязан перезаписывать, а не пропускать**. Выкладку запускает
|
||||
человек.
|
||||
|
||||
**Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23**
|
||||
задачей `config-test-headers-login`. При включённом предохранителе
|
||||
`[server] debug` заголовки входа ставит не прокси, а сам сервис значениями из
|
||||
секции `[auth] test_headers`: на машине разработчика прокси нет, а браузер
|
||||
заголовков не ставит. Узнавание при этом остаётся тем же и подставленного
|
||||
заголовка от пришедшего не отличает — отлаживается боевая ветка. Нормирует
|
||||
изъятие спека [access](../openspec/specs/access/spec.md).
|
||||
|
||||
Держится оно тремя вещами, и других нет: умолчание предохранителя —
|
||||
«выключено»; заполненная имитация при выключенном предохранителе роняет старт с
|
||||
именем ключа; боевой конфиг рендерится шаблоном Ansible, а не копируется с
|
||||
машины разработчика. Подставленный заголовок проходит тот же барьер доверенного
|
||||
адреса, что и пришедший, и судит адрес та же функция — но барьером отладочному
|
||||
входу это не служит: перечень доверенных адресов включению предохранителя не
|
||||
мешает.
|
||||
|
||||
**Боевая поломка машиной не исключена, и это названо прямо.** Сервис, поднятый в
|
||||
бою с включённым предохранителем и заполненной имитацией, поднимется на любом
|
||||
перечне доверенных адресов и назовёт своим именем всякого, чей запрос пришёл
|
||||
через обратный прокси, — то есть всякого, кто пришёл обычным путём. Адресного
|
||||
предохранителя у изъятия нет: требование петлевого перечня рассматривалось и
|
||||
снято решением владельца на чекпоинте задачи.
|
||||
|
||||
**`X-Forwarded-For` сервис читает сам, и правило чтения закрывает дописывание.**
|
||||
С 2026-08-22 адрес спрашивающего ограничитель частоты берёт из этого заголовка:
|
||||
иначе счётчик ведётся по адресу пира, а пир теперь всегда один — прокси, — и
|
||||
@@ -120,7 +143,7 @@ Telegram — связи чата с учётной записью сервис
|
||||
|
||||
| Вход | Канал | Кто может слать |
|
||||
| --- | --- | --- |
|
||||
| **Имя пришедшего, имя для показа и почта** | Заголовки `Remote-User`, `Remote-Name`, `Remote-Email` | Обратный прокси — и **всякий, кто дотянулся до сервиса с доверенного адреса**. Значение принимается: пустое, пробельное, длиннее 255 знаков и с управляющими знаками не узнают никого; **два значения одного заголовка** не узнают никого тоже. С недоверенного адреса заголовок не действует, и это идёт в журнал предупреждением с адресом пира, но без значения |
|
||||
| **Имя пришедшего, имя для показа и почта** | Заголовки `Remote-User`, `Remote-Name`, `Remote-Email` | Обратный прокси — и **всякий, кто дотянулся до сервиса с доверенного адреса**. Значение принимается: пустое, пробельное, длиннее 255 знаков и с управляющими знаками не узнают никого; **два значения одного заголовка** не узнают никого тоже. С недоверенного адреса заголовок не действует, и это идёт в журнал предупреждением с адресом пира, но без значения. Слать тройку может ещё и сам сервис — при включённом предохранителе `[server] debug`, значением из настроек; изъятие целиком описано в «Периметре» выше |
|
||||
| Аудиофайл и его имя | `POST /app/audiorecords`, multipart-поле `audio` | Любой узнанный; неузнанному — `401` до чтения тела. Имя доходит до колонки записи обрезанным по пределу и без управляющих знаков |
|
||||
| Идентификатор записи | `GET /app/audiorecords/{id}` и `/text` | Любой узнанный; неузнанному — `401`, одинаковый для заведённой и незаведённой записи |
|
||||
| Ключ страницы, размер страницы, состояние отбора | `GET /app/audiorecords`, параметры запроса | Любой узнанный; нечитаемый ключ и негодный размер дают `400`, а не молчаливую первую страницу |
|
||||
@@ -382,14 +405,16 @@ Storage, оттуда его читает SpeechKit. Третий путь —
|
||||
|
||||
- **Атака на сам сервер и на контур.** Компрометация хоста, прокси, Docker и
|
||||
Ansible — не наша граница.
|
||||
- **Машина разработчика и то, что он на ней поднимает.** В репозитории лежит
|
||||
`cmd/devtools` — оснастка разработчика; её подкоманда `proxy` встаёт на место
|
||||
контура: ставит заголовок `Remote-User` и переправляет запрос сервису, не
|
||||
проверяя ничего. С 2026-08-15 по 2026-08-22 ту же роль играл `cmd/oidcstub`,
|
||||
подставной провайдер OIDC. Двух вещей это не отменяет, и обе проверяемы: в
|
||||
образ оснастка не едет (ступень собирает `./cmd/transcriber` поимённо), а
|
||||
слушает петлевой адрес. Периметра выкладки она поэтому не касается; кто поднял
|
||||
её у себя в чужой сети, отвечает за это сам.
|
||||
- **Машина разработчика и то, что он на ней поднимает.** На место контура встаёт
|
||||
сам сервис: при включённом предохранителе `[server] debug` он подставляет
|
||||
заголовки входа значениями из конфига. Прежде эту роль играли отдельные
|
||||
процессы — `cmd/oidcstub` с 2026-08-15 по 2026-08-22 и подкоманда
|
||||
`cmd/devtools proxy` с 2026-08-22 по 2026-08-23; ни того, ни другой в
|
||||
репозитории больше нет. Периметра выкладки отладочный запуск не касается,
|
||||
пока предохранитель выключен, а выключен он по умолчанию; кто включил его у
|
||||
себя в чужой сети, отвечает за это сам. В оснастке `cmd/devtools` осталась
|
||||
одна подкоманда — `resume`, — и в образ она не едет: ступень собирает
|
||||
`./cmd/transcriber` поимённо.
|
||||
- **Злоупотребление со стороны пользователя из белого списка.** Приглашённому
|
||||
доверяем полностью.
|
||||
- **Достоверность расшифровки.** Подмена или искажение текста на стороне
|
||||
|
||||
Reference in New Issue
Block a user