документы: сведены расхождения, найденные сверкой канона

- один факт — один дом: рецепт локального входа, правило чтения
  X-Forwarded-For, уровень строки журнала и опись опор изъятия сведены к
  своим домам, копии заменены ссылками
- форма [auth.test_headers] выровнена по образцу конфига в семи местах;
  сценарии intake и archive перестали ссылаться на сессию, которой сервис
  не выдаёт
- поправлены протухшие факты: ключ объекта строит ULID, а не UUID; сверку
  адреса пира зовут трое, а не двое; обзор capability access знает о
  задаче 2026-08-23
This commit is contained in:
av
2026-08-23 16:41:06 +03:00
parent 11269c1567
commit 31ce520c5b
12 changed files with 65 additions and 66 deletions
+3 -5
View File
@@ -278,11 +278,9 @@ Node на машину **не ставится**: шаг сборки прило
пришедшего по заголовку, который на сервере ставит Caddy, а браузер заголовков пришедшего по заголовку, который на сервере ставит Caddy, а браузер заголовков
не ставит. Заголовок подставляет сам сервис — настройками, а не вторым не ставит. Заголовок подставляет сам сервис — настройками, а не вторым
процессом: рецепт из трёх правок записан связным блоком в процессом: рецепт из трёх правок записан связным блоком в
`config.example.toml`, под перечнем доверенных адресов. Коротко: пара петлевых `config.example.toml`, под перечнем доверенных адресов. Приложение при этом
адресов в перечень, `[server] debug = true`, раскомментированная секция открывают по адресу сервиса, второго порта нет. Заполненная имитация при
`[auth.test_headers]` с ключом `Remote-User`. Приложение при этом открывают по выключенном предохранителе роняет старт с именем ключа. Ключей боевого
адресу сервиса, второго порта нет. Заполненная имитация при выключенном
предохранителе роняет старт с именем ключа. Ключей боевого
провайдера на машине разработчика не нужно вовсе — их больше нет и в конфиге. провайдера на машине разработчика не нужно вовсе — их больше нет и в конфиге.
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage - **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён — оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
+3 -8
View File
@@ -52,15 +52,10 @@
сервис по своим настройкам. Второго процесса для этого не нужно: приложение сервис по своим настройкам. Второго процесса для этого не нужно: приложение
открывают по адресу сервиса. открывают по адресу сервиса.
Рецепт — три правки `config.toml` сверху вниз: Рецепт целиком — связным блоком в `config.example.toml`, под перечнем доверенных
адресов: там названы три правки, принимаемые имена заголовков и цена включения.
1. добавить в перечень доверенных адресов пару петлевых — `127.0.0.1` и `::1`; Заполненная секция имитации при выключенном предохранителе роняет
2. поставить в секции `[server]` ключ `debug = true`;
3. раскомментировать секцию `[auth.test_headers]` и назвать в ней `Remote-User`.
Тот же рецепт записан связным блоком в `config.example.toml`, под перечнем
доверенных адресов, — там же названы принимаемые имена заголовков и цена
включения. Заполненная секция имитации при выключенном предохранителе роняет
старт с именем ключа: сервис с включённым предохранителем называет пришедшего старт с именем ключа: сервис с включённым предохранителем называет пришедшего
сам, никого не спросив, и в бою этот ключ стоит `false`. сам, никого не спросив, и в бою этот ключ стоит `false`.
@@ -1,7 +1,7 @@
# Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC # Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC
- **Дата:** 2026-08-22 - **Дата:** 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
View File
@@ -59,7 +59,13 @@
`trusted-header-login` — вместе с куками, сессией и её сроком. Здесь же разграничение записей по владельцу: принятая запись `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. Задача, которая его трогает, дописывает спеку своей capability.
@@ -84,7 +90,7 @@
- **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему — - **Подставной собеседник в боевом бинарнике объявлен своим ключом.** Дом ему —
код или оснастка; в боевом бинарнике он появляется только отдельным решением код или оснастка; в боевом бинарнике он появляется только отдельным решением
владельца и только под ключом, названным своим предметом: имитацию заголовков владельца и только под ключом, названным своим предметом: имитацию заголовков
входа объявляет секция `[auth] test_headers`, подмену распознавания — правка входа объявляет секция `[auth.test_headers]`, подмену распознавания — правка
кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом, кода (`internal/adapter/recognizer/memory.go`). Ключ, названный общим словом,
обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт обрастает следствиями молча, и выключить его перестаёт означать «сервис ведёт
себя как в бою». Предохранитель `[server] debug` вторым именем собеседнику при себя как в бою». Предохранитель `[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/controller/worker` | Пул одинаковых потоков: каждый берёт любую пригодную запись и опрашивает базу. Число — настройкой, ноль законен |
| Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи | | Сервис расшифровки | `internal/service` | Конвейер: приём, приведение, отправка, опрос, завершение. Шаг выбирается по рубежу записи |
| Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности | | Конвертер и метаданные | `internal/adapter/{converter,metaviewer}/ffmpeg` | `ffmpeg` в ogg/vorbis, `ffprobe` для длительности |
@@ -184,7 +190,8 @@
## Внешние границы и форматы ## Внешние границы и форматы
- **Yandex Object Storage.** S3-совместимый, клиент `aws-sdk-go-v2` с - **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`, модель - **Yandex SpeechKit v3.** gRPC, `stt.api.cloud.yandex.net:443`, модель
`deferred-general`, авторизация заголовком `Api-Key`. Распознавание `deferred-general`, авторизация заголовком `Api-Key`. Распознавание
асинхронное: запрос возвращает идентификатор операции, готовность опрашивается асинхронное: запрос возвращает идентификатор операции, готовность опрашивается
@@ -192,6 +199,10 @@
- **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`. - **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`.
- **SQLite через `modernc.org/sqlite`.** Драйвер на чистом Go: CGO сборке не - **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`.** Шаги схемы — библиотекой, а не командной - **`github.com/pressly/goose/v3`.** Шаги схемы — библиотекой, а не командной
строкой: перечень шагов приходит провайдеру доводом, накат идёт при старте. строкой: перечень шагов приходит провайдеру доводом, накат идёт при старте.
Исключающей блокировки под SQLite библиотека не даёт, и замок каталога данных Исключающей блокировки под SQLite библиотека не даёт, и замок каталога данных
@@ -284,8 +295,8 @@
| Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути, уровень журнала и область действия узнавания | | Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути, уровень журнала и область действия узнавания |
| Узнавание предъявителя | `sqlite.UserRepository.EnsureUser` — поиск учётной записи по логину у провайдера и заведение при первом обращении. Дом правила один и лежит в хранилище, а не в транспорте: второй способ представиться (личные токены) возьмёт этот же метод, а уложенное куском в слой оно разошлось бы двумя копиями. Транспорт читает заголовок, судит адрес пира и зовёт метод интерфейсом `contract.UserRepository``internal/controller/http.TrustedHeaderIdentity` | | Узнавание предъявителя | `sqlite.UserRepository.EnsureUser` — поиск учётной записи по логину у провайдера и заведение при первом обращении. Дом правила один и лежит в хранилище, а не в транспорте: второй способ представиться (личные токены) возьмёт этот же метод, а уложенное куском в слой оно разошлось бы двумя копиями. Транспорт читает заголовок, судит адрес пира и зовёт метод интерфейсом `contract.UserRepository``internal/controller/http.TrustedHeaderIdentity` |
| Приём значения заголовка | `internal/entity.AcceptProviderLogin`, `AcceptDisplayName`, `AcceptEmail` — правило одно на все способы представиться | | Приём значения заголовка | `internal/entity.AcceptProviderLogin`, `AcceptDisplayName`, `AcceptEmail` — правило одно на все способы представиться |
| Имена заголовков входа | `internal/controller/http.IdentityHeaderNames` вместе с константами рядом — тройка `Remote-*` перечисляется отсюда, а не по месту. она же порождает набор имён, принимаемых секцией `[auth] test_headers`: ключ, не совпавший ни с одним, роняет старт | | Имена заголовков входа | `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`, `identity.go``peerAddress` и `isTrusted`. Зовут их узнавание, подстановка заголовков отладочного запуска и ограничитель частоты. Свой сверщик разошёлся бы с общим молча — разбор разворачивает IPv4 в оболочке IPv6, и разница пришлась бы ровно на те адреса, ради которых он заводится |
| Ограничитель частоты | `internal/controller/http.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса | | Ограничитель частоты | `internal/controller/http.RateLimit` — бюджет по адресу спрашивающего под корнем приложения; из его чисел выводится объявляемая частота опроса |
Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось. Единых точек, которых **нет** и которые ожидались бы, сегодня не осталось.
+4 -6
View File
@@ -63,12 +63,10 @@ force_shutdown_timeout = <N> # ждать остановки ворке
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
входом. входом.
Там же, комментарием под секцией, стоит **рецепт локального входа одним связным Там же, комментарием под секцией, стоит рецепт локального входа **одним связным
блоком**: три правки сверху вниз — пара петлевых адресов в перечень доверенных, блоком**, а не тремя комментариями по месту: правки связаны между собой, и
`[server] debug = true`, раскомментированная секция `[auth.test_headers]` с применённая порознь любая из них роняет старт либо оставляет сервис никого не
ключом `Remote-User`. Блок один, а не три комментария по месту: правки связаны узнающим. Рабочей строкой в образце стоит боевое значение —
между собой, и применённая порознь любая из них роняет старт либо оставляет
сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение —
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
дойти, и два рабочих значения в одном файле читались бы как выбор без указания, дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
+4 -4
View File
@@ -87,10 +87,10 @@ stdlib-логом в поток ошибок. Это выбор, а не дол
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем. *Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит. Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
*Изъятие:* строка о подставленных заголовках входа *Изъятие:* строка о подставленных заголовках входа адресована разработчику, а
(`internal/controller/http/substitute.go`) адресована разработчику, а идёт на идёт на `INFO` — уровень и его довод нормирует спека
`INFO`: `DEBUG` включить нечем, а в бою она не пишется вовсе — подстановку [access](../../openspec/specs/access/spec.md), «Отладочный запуск виден в
держит выключенный умолчанием предохранитель `[server] debug`. журнале».
## Время ## Время
+2 -3
View File
@@ -424,9 +424,8 @@ Crockford. Выдаёт их приложение единой точкой `int
и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и
восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка — восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка —
верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он
ограничивает. Сама цепочка читается справа налево с отбрасыванием доверенных ограничивает. Как читается цепочка — спека
адресов, поэтому дописывающий прокси правилом покрыт; почему так — [archive](../openspec/specs/archive/spec.md); здесь только числа бюджета.
[security.md](security.md), «Периметр».
Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа
провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ
+1 -1
View File
@@ -345,7 +345,7 @@ API и имя не откатываются обратной правкой по
**Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде **Вход живой прогон теперь проверяет целиком, и это сдвиг 2026-08-22.** Прежде
сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по сессию в прогоне выдать было нечем; теперь заголовок ставит сам сервис по
секции `[auth] test_headers` под предохранителем `[server] debug` — прежде секции `[auth.test_headers]` под предохранителем `[server] debug` — прежде
`cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание, `cmd/devtools proxy`, убранный 2026-08-23, — и живьём проверяются узнавание,
заведение учётной записи первым обращением, отказ с недоверенного адреса и заведение учётной записи первым обращением, отказ с недоверенного адреса и
отказ старта на пустом перечне. отказ старта на пустом перечне.
+12 -13
View File
@@ -85,10 +85,12 @@ Telegram — связи чата с учётной записью сервис
**Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23** **Изъятие из барьера одно — отладочный запуск, и заведено оно 2026-08-23**
задачей `config-test-headers-login`. При включённом предохранителе задачей `config-test-headers-login`. При включённом предохранителе
`[server] debug` заголовки входа ставит не прокси, а сам сервис значениями из `[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` сервис читает сам, и правило чтения закрывает дописывание.** **`X-Forwarded-For` сервис читает сам, и правило чтения закрывает дописывание.**
С 2026-08-22 адрес спрашивающего ограничитель частоты берёт из этого заголовка: Как именно читается цепочка, нормирует спека
иначе счётчик ведётся по адресу пира, а пир теперь всегда один — прокси, — и [archive](../openspec/specs/archive/spec.md), «Адреса приложения живут своим
бюджет становится общим на весь сервис. Цепочка читается **справа налево**, пространством». Отсюда периметровое следствие: прокси, дописывающий
доверенные адреса отбрасываются, и ключом становится первый недоверенный: левым `X-Forwarded-For` к присланному, этим правилом покрыт, и требования
значением распоряжается сам спрашивающий, а правое приписал ближайший к нам «перезаписывать, а не дописывать» у сервиса к нему нет — в отличие от `Remote-*`.
прокси. Заголовок читается всеми строками, а не одной: цепочка законно приходит Барьером узнавания заголовок при этом не служит: кто пришёл, решает адрес самого
несколькими. Прокси, дописывающий `X-Forwarded-For` к присланному, этим правилом соединения.
покрыт, и требования «перезаписывать, а не дописывать» у сервиса к нему нет — в
отличие от `Remote-*`. Барьером узнавания заголовок при этом не служит: кто
пришёл, решает адрес самого соединения.
**Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.** **Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.**
Перечень задаёт, чьему `Remote-User` верить, и всякий, кто дотянулся до сервиса Перечень задаёт, чьему `Remote-User` верить, и всякий, кто дотянулся до сервиса
+14 -15
View File
@@ -81,12 +81,10 @@
«Предохранитель отладки включает только подстановку заголовков»; снимать их «Предохранитель отладки включает только подстановку заголовков»; снимать их
поодиночке нельзя: барьер держится всеми разом. поодиночке нельзя: барьер держится всеми разом.
Держится изъятие **умолчанием, а не машиной**: предохранитель по умолчанию Держится изъятие умолчанием предохранителя «выключено» и отказом старта при
выключен, а заполненная имитация без него роняет старт. Боевой перечень заполненной имитации без него. Что остаётся между боевой выкладкой и открытым
доверенных адресов включению предохранителя не мешает — сервис, поднятый в бою с входом целиком, включая опору вне репозитория, перечисляет модель угроз —
включённым предохранителем и заполненной имитацией, назовёт своим именем всякого, `docs/security.md`, «Периметр».
чей запрос пришёл через обратный прокси, то есть всякого, кто пришёл обычным
путём.
#### Scenario: Названный провайдером получает доступ #### Scenario: Названный провайдером получает доступ
@@ -275,14 +273,15 @@ MUST отвечать отказом `401`, когда пришедший не
**Изъятие одно — отладочный запуск.** При включённом предохранителе **Изъятие одно — отладочный запуск.** При включённом предохранителе
`[server] debug` заголовок входа ставит не прокси, а сам сервис значением из `[server] debug` заголовок входа ставит не прокси, а сам сервис значением из
секции `[auth] test_headers`; узнавание при этом остаётся тем же и подставленного секции `[auth.test_headers]`; узнавание при этом остаётся тем же и подставленного
заголовка от пришедшего не отличает. Условия, при которых источник этот законен, заголовка от пришедшего не отличает. Условия, при которых источник этот законен,
и проверки старта, которыми он держится, стоят требованиями «Отладочный запуск и проверки старта, которыми он держится, перечислены требованием «Кого пускать,
называет пришедшего настройками», «Настройка, открывающая вход всем, роняет решает провайдер».
старт», «Отладочный запуск виден в журнале» и «Предохранитель отладки включает
только подстановку заголовков». Держится изъятие умолчанием предохранителя Держится изъятие умолчанием предохранителя «выключено» и отказом старта при
«выключено» и отказом старта при заполненной имитации без него; боевой перечень заполненной имитации без него. Что остаётся между боевой выкладкой и открытым
доверенных адресов включению предохранителя не мешает. входом целиком, включая опору вне репозитория, перечисляет модель угроз —
`docs/security.md`, «Периметр».
Заголовку сервис MUST верить только тогда, когда запрос пришёл с адреса из Заголовку сервис MUST верить только тогда, когда запрос пришёл с адреса из
объявленного перечня доверенных, и адрес этот MUST браться у самого соединения, объявленного перечня доверенных, и адрес этот MUST браться у самого соединения,
@@ -591,7 +590,7 @@ MUST не выдавать вовсе — ни куки, ни токена се
### Requirement: Отладочный запуск называет пришедшего настройками ### Requirement: Отладочный запуск называет пришедшего настройками
Сервис SHALL подставлять запросу заголовки входа значениями из секции Сервис SHALL подставлять запросу заголовки входа значениями из секции
`[auth] test_headers`, когда включён предохранитель `[server] debug`, и MUST `[auth.test_headers]`, когда включён предохранитель `[server] debug`, и MUST
делать это так, чтобы узнавание не отличало подставленный заголовок от делать это так, чтобы узнавание не отличало подставленный заголовок от
поставленного обратным прокси. Ветка кода, которой узнаётся пришедший, обязана поставленного обратным прокси. Ветка кода, которой узнаётся пришедший, обязана
быть той же, что работает в бою: отладке подлежит боевой путь, а не его быть той же, что работает в бою: отладке подлежит боевой путь, а не его
@@ -685,7 +684,7 @@ MUST не выдавать вовсе — ни куки, ни токена се
Умолчания названы нормой, а не образцом конфига. Отсутствие ключа `[server] Умолчания названы нормой, а не образцом конфига. Отсутствие ключа `[server]
debug` MUST читаться как выключенный предохранитель, а отсутствие секции debug` MUST читаться как выключенный предохранитель, а отсутствие секции
`[auth] test_headers` — как пустая секция: конфиг сегодняшнего дня, не тронутый `[auth.test_headers]` — как пустая секция: конфиг сегодняшнего дня, не тронутый
ни на байт, обязан вести себя ровно как вёл. Ошибка разбора значения MUST ни на байт, обязан вести себя ровно как вёл. Ошибка разбора значения MUST
кончаться отказом старта, а не прочтением «включено». кончаться отказом старта, а не прочтением «включено».
+1 -1
View File
@@ -265,7 +265,7 @@ MUST называть в нём потолок размера одной зап
#### Scenario: Потолок размера равен тому, которым сервис отвергает #### Scenario: Потолок размера равен тому, которым сервис отвергает
- **GIVEN** человек вошёл и предъявил сессию - **GIVEN** человек узнан
- **WHEN** он спрашивает пределы сервиса - **WHEN** он спрашивает пределы сервиса
- **THEN** потолок размера в ответе равен потолку, которым сервис ограничивает - **THEN** потолок размера в ответе равен потолку, которым сервис ограничивает
тело запроса приёма тело запроса приёма
+3 -3
View File
@@ -302,19 +302,19 @@ MUST ограничивать его длину и MUST убирать из не
#### Scenario: Имя доходит до записи #### Scenario: Имя доходит до записи
- **GIVEN** отправитель предъявил сессию - **GIVEN** отправитель узнан
- **WHEN** он шлёт запись с именем `разговор.mp3` - **WHEN** он шлёт запись с именем `разговор.mp3`
- **THEN** колонка имени файла у заведённой записи несёт `разговор.mp3` - **THEN** колонка имени файла у заведённой записи несёт `разговор.mp3`
#### Scenario: Заголовок принятой записи пуст #### Scenario: Заголовок принятой записи пуст
- **GIVEN** отправитель предъявил сессию - **GIVEN** отправитель узнан
- **WHEN** он шлёт запись с именем `разговор.mp3` - **WHEN** он шлёт запись с именем `разговор.mp3`
- **THEN** колонка заголовка у заведённой записи пуста - **THEN** колонка заголовка у заведённой записи пуста
#### Scenario: Длинное и грязное имя приходит обрезанным и очищенным #### Scenario: Длинное и грязное имя приходит обрезанным и очищенным
- **GIVEN** отправитель предъявил сессию - **GIVEN** отправитель узнан
- **WHEN** он шлёт запись, чьё имя длиннее предела и несёт управляющие знаки - **WHEN** он шлёт запись, чьё имя длиннее предела и несёт управляющие знаки
- **THEN** колонка имени файла несёт имя не длиннее предела - **THEN** колонка имени файла несёт имя не длиннее предела
- **AND** управляющих знаков в нём нет - **AND** управляющих знаков в нём нет