HTTP API закрыт за вход через OIDC у Authelia

- шаг схемы закрывает поверхность, которую хранилище приносит открытой:
  собственную регистрацию, вход по паролю и одноразовый код — без этого
  закрытие приёма обходилось двумя запросами
- продление сессии выключено, срок семь суток: иначе отзыв доступа у
  провайдера до сервиса не доходит никогда
- файл записи отдаётся вошедшему по токену файла — пересмотр
  ADR-2026-08-12-file-link-open-but-not-logged
This commit is contained in:
av
2026-08-12 17:44:22 +03:00
parent d676df8a27
commit c44f0e7582
37 changed files with 3997 additions and 65 deletions
+64 -20
View File
@@ -2,14 +2,20 @@
## Периметр
**Сервис открыт наружу: HTTP-порт опубликован в интернет через обратный прокси, и
аутентификации не делает ни прокси, ни само приложение.** Находки строятся против
этого — сегодняшнего — периметра.
**Сервис открыт наружу, но не анонимен: HTTP-порт опубликован в интернет через
обратный прокси, а приём записи, опрос готовности и файл записи требуют входа
через OIDC у Authelia.** Вход развёрнут задачей `oidc-login` 2026-08-12. Открыты
без входа только проба здоровья и метрики. Находки строятся против этого —
сегодняшнего — периметра.
Целевой периметр: те же порты наружу, но вход через OIDC у Authelia, отдельный
вход для программ по личным токенам, два уровня доступа — пользователь видит
свои записи, владелец сервиса ещё и страницу расхода. Он **не** развёрнут;
описанное ниже разграничение доступа относится только к Telegram.
Целевой периметр добавляет к нему отдельный вход для программ по личным токенам
и два уровня доступа — пользователь видит свои записи, владелец сервиса ещё и
страницу расхода. **Разграничения по владельцу нет:** всякий вошедший видит все
записи и все расшифровки, как видел их прежде аноним. Его заводит задача
`record-ownership`.
Разграничение доступа в Telegram осталось прежним — белым списком, и с учётной
записью приложения он не связан.
**Целевой периметр шире сегодняшнего не только входом.** Содержимое записи
начинает уходить на три новые стороны — языковой модели, в канал уведомлений и
@@ -28,9 +34,18 @@
администраторов. Задачи в беклоге у этого нет — работа принадлежит выкладке, а
она вне модели («Что вне модели», строка про контур).
Отсюда главное следствие, из которого читается всё остальное: **`POST /api/audio`
доступен кому угодно из интернета**. Отправитель не назван, не ограничен по числу
запросов и не ограничен по размеру файла.
**Четвёртый сдвиг — секрет клиента поселился в базе.** Задача `oidc-login`
2026-08-12 кладёт адреса провайдера, идентификатор клиента и его секрет в
настройки коллекции пользователей, приводя их к конфигу при каждом подъёме
(применённый шаг схемы не переписывается, и положенный им секрет не пережил бы
ротации). Инвариант проекта запрещает секрету попадать в git, в лог, в ответ и в
`error_text`; база в этом перечне не значится, и запрет не нарушен. Но место
новое: **чтение файла базы теперь равносильно чтению секрета клиента**.
Отсюда главное следствие, из которого читается всё остальное: **`POST
/api/audio` требует входа, а число запросов и размер файла по-прежнему ничем не
ограничены**. Вошедший не ограничен ни в том, ни в другом, и тратит наши деньги
на распознавание столько, сколько захочет.
## Недоверенный вход
@@ -94,9 +109,12 @@ Telegram отправителю.
каталогов, но это единственное, что стоит между входом и именем файла.
- **Ключ объекта в Object Storage** — то же имя файла, то есть UUID с
расширением. Бакет один на все записи, префикса по пользователю нет.
- **Ссылка на файл** — `/api/files/<коллекция>/<запись>/<имя>`. Поле файла не
помечено защищённым, поэтому ссылка сама по себе и есть право пройти по ней, а
отзыва у неё нет. Отсюда запрет: **имя файла в хранилище в журнал не пишется**
- **Ссылка на файл** — `/api/files/<коллекция>/<запись>/<имя>`. Поле файла
помечено защищённым задачей `oidc-login` 2026-08-12: пройти по ссылке теперь
можно только с коротким токеном файла, который выдаётся по сессии, и запрос
без него получает «не найдено». Сама ссылка отзыва по-прежнему не имеет —
токен сужает круг и живёт недолго, но выданное не отзывается. Отсюда запрет
остаётся: **имя файла в хранилище в журнал не пишется**
— иначе строка журнала вместе с идентификатором записи собирала бы ссылку
целиком и работала бы бессрочно. В журнал идёт расширение своим полем.
- **Идентификатор задачи** — 15 знаков, выдаёт хранилище. Он же единственное,
@@ -124,18 +142,44 @@ Telegram отправителю.
автора сообщения (`update.Message.From.String()`, то есть `@username` либо имя
с фамилией), а не с числовым идентификатором. Имя пользователя Telegram
меняется владельцем в любой момент: список привязан к изменяемому значению.
- **HTTP API** — ничего. Ни ключа, ни сессии, ни ограничения по адресу.
- **Метрики и здоровье** — `GET /metrics` и `GET /health` открыты вместе с
остальным.
- **HTTP API** — сессия, заведённая входом через OIDC у Authelia. Предъявляется
кукой `transcriber_session`, живёт семь суток, обесценивается выходом.
Продление сессии закрыто: с ним предъявитель менял бы своё значение на новое
бессрочно, и семисуточный срок — единственное, чем отзыв доступа у провайдера
доходит до сервиса, — не значил бы ничего.
Предъявленный заголовок `Authorization` принимается тоже — это та же сессия и
та же проверка, но она названа здесь отдельно, потому что это второй способ
предъявить ту же сессию.
- **Файл записи** — короткий токен файла, который узнанный отправитель берёт у
хранилища, предъявив сессию. Поле файла помечено защищённым, правило просмотра
коллекции пускает всякого вошедшего, и ссылка `/api/files/...` перестала быть
правом пройти по ней. Браузер с одной лишь кукой файла не получает: порядок
здесь «сессия → токен файла → ссылка».
- **Кто допущен** — **решает Authelia, а не сервис.** Своей проверки группы
приложение не делает: кого пускать, определяет правило провайдера на этого
клиента. Правило живёт **вне репозитория**, в настройках выкладки, и по коду
его не проверить. Клиент, настроенный слишком широко, открывает сервис
всякому, у кого есть учётная запись в общей Authelia. Решение владельца от
2026-08-12.
- **Заведение учётной записи** — только входом у провайдера. Собственное
создание записи, вход по паролю, одноразовый код и восстановление доступа
выключены шагом схемы: хранилище заводит коллекцию пользователей открытой, и
без этого закрытия вход обходился бы двумя запросами.
- **Метрики и здоровье** — `GET /metrics` и `GET /health` открыты без сессии:
её нет ни у пробы, ни у сборщика. Наружу их закрывает правило обратного
прокси — работа выкладки, и сервис на неё не полагается: содержимого записей
эти адреса не несут.
Владения записью в модели данных нет: у задачи нет пользователя. Пока API
анонимен, знание UUID задачи и есть право её читать.
Владения записью в модели данных по-прежнему нет: у задачи нет пользователя.
Знание UUID задачи и есть право её читать — теперь для всякого вошедшего, а не
для всякого встречного.
Целевой периметр заводит четыре механизма вместо одного белого списка:
Целевой периметр заводит четыре механизма вместо одного белого списка; первый из
них уже стоит:
| Механизм | Что даёт | Чья задача |
| --- | --- | --- |
| Сессия OIDC у Authelia | Право открыть приложение и его эндпоинты | `oidc-login` |
| Сессия OIDC у Authelia | Право открыть приложение и его эндпоинты**сделано 2026-08-12** | `oidc-login` |
| Владелец у задачи и файла | Чужая запись по её идентификатору отвечает «не найдено» | `record-ownership` |
| Личный токен | Права своего владельца программе, без браузерной сессии | `api-tokens` |
| Признак владельца сервиса | Страницу расхода и сводку по всем пользователям | `admin-stats-screen` |