HTTP API закрыт за вход через OIDC у Authelia
- шаг схемы закрывает поверхность, которую хранилище приносит открытой: собственную регистрацию, вход по паролю и одноразовый код — без этого закрытие приёма обходилось двумя запросами - продление сессии выключено, срок семь суток: иначе отзыв доступа у провайдера до сервиса не доходит никогда - файл записи отдаётся вошедшему по токену файла — пересмотр ADR-2026-08-12-file-link-open-but-not-logged
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Кого пускать в сервис, решает правило провайдера, а не сервис
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-oidc-login/design.md](../../openspec/changes/archive/2026-08-12-oidc-login/design.md),
|
||||
раздел «Кого пускать, решает провайдер, а не сервис»
|
||||
|
||||
## Решение
|
||||
|
||||
Сервис пускает всякого, кого пропустил провайдер, и **своей проверки допуска не
|
||||
делает**. Кто допущен, определяет правило Authelia на этого клиента — настройка
|
||||
выкладки, лежащая вне репозитория.
|
||||
|
||||
## Почему
|
||||
|
||||
Authelia — общий провайдер контура, а не выделенный под этот сервис: учётная
|
||||
запись в ней есть у всякого, кому её завели ради любого другого сервиса на том же
|
||||
сервере. Ревью дизайна назвало следствие прямо: механизм, приглашающий «второго
|
||||
человека», приглашает всех, кто уже есть у провайдера.
|
||||
|
||||
Очевидный ответ — проверять принадлежность к названной в конфиге группе своим
|
||||
кодом. Владелец от него отказался: это завело бы **второе место**, где решается
|
||||
допуск, и решать его пришлось бы в двух местах согласованно.
|
||||
|
||||
Цена отказа названа в источнике и повторена в модели угроз:
|
||||
|
||||
> Правило живёт вне репозитория, в настройках выкладки, и сервис на него
|
||||
> полагается так же, как полагается на обратный прокси в части панели
|
||||
> администратора. Настроенный слишком широко клиент открывает сервис всем, у кого
|
||||
> есть учётная запись в общей Authelia, — и проверить это по коду нельзя.
|
||||
|
||||
Запись заводится как **намеренный отказ от очевидного подхода**: проверку группы
|
||||
предложат снова, и без записанной причины она выглядит бесплатной.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` допуск решается в одном месте, а не в двух; изменение круга допущенных не
|
||||
требует ни правки кода, ни выкладки.
|
||||
- `+` сервис не читает из ответа провайдера ничего сверх нужного для заведения
|
||||
записи — ни групп, ни ролей.
|
||||
- `−` защита сервиса стала свойством настройки, лежащей в другом репозитории, и
|
||||
ревью её проверить не может: ни один проход не увидит, что клиент настроен
|
||||
слишком широко.
|
||||
- `−` ошибка в настройке клиента не имеет наблюдаемого признака внутри сервиса:
|
||||
посторонний, которого пропустила Authelia, выглядит как законный пользователь.
|
||||
- `−` разграничения по владельцу нет, поэтому цена ошибки в настройке — все
|
||||
записи и все расшифровки разом, а не одна учётная запись. Сузит это
|
||||
`record-ownership`.
|
||||
@@ -3,6 +3,7 @@
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md](../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md),
|
||||
раздел «Поле файла не помечаем защищённым, но ссылка не уезжает в журнал»
|
||||
- **Статус:** заменено на [ADR-2026-08-12-protected-file-behind-session](ADR-2026-08-12-protected-file-behind-session.md)
|
||||
|
||||
## Решение
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Код провайдера меняется на сессию вызовом собственного адреса хранилища внутри процесса
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-oidc-login/design.md](../../openspec/changes/archive/2026-08-12-oidc-login/design.md),
|
||||
раздел «Вход и возврат ведёт наш код, разбор ответа — хранилище»
|
||||
|
||||
## Решение
|
||||
|
||||
Обработчик возврата от провайдера зовёт **собственный адрес хранилища**
|
||||
`auth-with-oauth2` внутри процесса, через его же роутер, а не по сети и не
|
||||
разбирая ответ провайдера своими руками.
|
||||
|
||||
## Почему
|
||||
|
||||
Решение [ADR-2026-08-11-pocketbase-storage-with-admin-panel](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)
|
||||
отдало разбор ответа провайдера хранилищу: только тогда учётные записи заводятся
|
||||
сами и видны в панели. Это решение не пересматривается — пересматривается способ
|
||||
до него дотянуться.
|
||||
|
||||
Проверка исходников библиотеки версии 0.39.10 показала, что обмен наружу не
|
||||
экспортирован: он живёт неэкспортированной функцией за собственным маршрутом.
|
||||
Остались три формы, и владелец выбрал первую:
|
||||
|
||||
> (а) внутрипроцессный вызов собственного маршрута `auth-with-oauth2`: решение
|
||||
> 2026-08-11 соблюдено дословно, цена — петля «наш обработчик → наш роутер → наш
|
||||
> обработчик», разбор JSON-ответа и потеря типизированной ошибки; (б) сборка
|
||||
> обмена из экспортированных кусков с сохранением записи и связи через `app.Save`:
|
||||
> прямой код без петли, цена — пересмотр решения 2026-08-11 отдельным ADR; (в)
|
||||
> отложить вход до появления фронтенда.
|
||||
|
||||
Запись заводится как **намеренный отказ от очевидного подхода**: собрать обмен
|
||||
своими руками выглядит проще и дешевле, и предложение вернётся, если причина не
|
||||
записана.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` разбор ответа провайдера, заведение учётной записи и связь её с внешним
|
||||
провайдером остаются за хранилищем — решение 2026-08-11 соблюдено дословно, а
|
||||
не «по духу».
|
||||
- `+` наш код не знает ни одного поля ответа провайдера: обновление библиотеки
|
||||
под смену формата ответа доезжает само.
|
||||
- `−` петля через собственный роутер: обработчик зовёт сервис, частью которого
|
||||
сам является. Это новый для проекта вид узла, и его придётся объяснять на
|
||||
каждом следующем изменении.
|
||||
- `−` ответ разбирается текстом, типизированная ошибка теряется: причина отказа
|
||||
обмена доступна только кодом состояния.
|
||||
- `−` роутер хранилища пришлось собирать **один раз** и держать полем: его
|
||||
сборка вешает обработчики на само приложение и без идентификатора, поэтому
|
||||
повторная не заменяет прежние. Ревью кода нашло это построенным путём —
|
||||
анонимный запрос копил обработчики без предела, а каждое сохранение задачи
|
||||
конвейером проходило по всем накопленным.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-oidc-login/design.md](../../openspec/changes/archive/2026-08-12-oidc-login/design.md),
|
||||
раздел «Что изменило ревью кода», плюс отчёт триажа
|
||||
[../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
|
||||
пункт 3
|
||||
|
||||
## Решение
|
||||
|
||||
Поле файла в хранилище **помечается защищённым**, а правило просмотра коллекции
|
||||
файлов пускает всякого узнанного. Ссылка `/api/files/<коллекция>/<запись>/<имя>`
|
||||
перестаёт быть правом пройти по ней: нужен короткий токен файла, который берут,
|
||||
предъявив сессию.
|
||||
|
||||
Запись заменяет [ADR-2026-08-12-file-link-open-but-not-logged](ADR-2026-08-12-file-link-open-but-not-logged.md).
|
||||
|
||||
## Почему
|
||||
|
||||
Прежнее решение было обусловленным и само назвало условие своего пересмотра:
|
||||
|
||||
> Решение действует до разграничения доступа: задачи `oidc-login` и
|
||||
> `record-ownership` меняют условие, и тогда пометку стоит пересмотреть новой
|
||||
> записью.
|
||||
|
||||
Условие наступило. Прежний довод — «право прочитать задачу даёт знание её
|
||||
идентификатора, и файл встаёт вровень с `GET /api/status/:id`» — держался на том,
|
||||
что опрос готовности открыт анонимно. Этот change закрывает опрос за вход, и
|
||||
файл, оставшийся открытым, стал бы единственным анонимным путём к содержимому
|
||||
записи — самому чувствительному, что есть у проекта.
|
||||
|
||||
Вторая половина прежнего решения остаётся в силе: имя файла в журнал по-прежнему
|
||||
не пишется. Защищённое поле сужает право пройти, но не отменяет запрета —
|
||||
строка журнала со ссылкой собирала бы половину ключа.
|
||||
|
||||
Пометки самой по себе оказалось мало, и это выяснило ревью кода прогоном:
|
||||
защищённый файл судится **и** токеном, **и** правилом просмотра коллекции, а
|
||||
незаданное правило означает «только владелец панели». Файл не получал ни аноним,
|
||||
ни вошедший — сценарий спеки не исполнялся вовсе. Правило назначено тем же шагом
|
||||
схемы.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` содержимое записи перестало быть доступным по одному знанию ссылки; после
|
||||
закрытия API это был последний анонимный путь к нему.
|
||||
- `+` условие, названное прежней записью, отработало как задумано: решение
|
||||
пересмотрено записью, а не молча.
|
||||
- `−` ссылка усложнилась для потребителя: браузер с одной кукой файла не
|
||||
получает, нужен порядок «сессия → токен файла → ссылка». Будущее приложение
|
||||
обязано этот шаг делать, и задача про прослушивание записи начинается с него.
|
||||
- `−` разграничения по владельцу нет: токен файла берёт всякий вошедший, и по
|
||||
ссылке он получит **любую** запись, а не только свою. Сужение приносит
|
||||
`record-ownership`; до неё круг сузился с «кто угодно из интернета» до «кто
|
||||
угодно из вошедших», и это меньше, чем кажется.
|
||||
- `−` отзыва у выданного токена нет, как не было у ссылки; смягчает только его
|
||||
короткий срок.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Сессия живёт семь суток и не продлевает саму себя
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-oidc-login/design.md](../../openspec/changes/archive/2026-08-12-oidc-login/design.md),
|
||||
раздел «Что изменило ревью кода», плюс отчёт триажа
|
||||
[../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
|
||||
пункт 6
|
||||
|
||||
## Решение
|
||||
|
||||
Срок жизни сессии — **семь суток**, назначается при каждом подъёме сервиса.
|
||||
Продление сессии **выключено**: адрес, которым хранилище меняет предъявленное
|
||||
значение на новое, закрыт слоем приложения.
|
||||
|
||||
## Почему
|
||||
|
||||
Умолчание хранилища — пять суток и продлеваемая сессия. Второе делает первое
|
||||
бессмысленным, и это выяснило ревью кода замером: предъявитель одного живого
|
||||
значения продлевает себе доступ бессрочно, никуда не входя.
|
||||
|
||||
Значение имеет то, на чём держится вся остановка перерасхода. Паспорт опирается
|
||||
на **отзыв доступа в Authelia** как на способ остановить того, кто тратит слишком
|
||||
много. Но сервис после входа к провайдеру не обращается: подпись сессии считается
|
||||
от значений в базе, и отзыв у провайдера до сервиса доходит **только** истечением
|
||||
срока. При живом продлении не доходит никогда — человек, которому закрыли доступ,
|
||||
сохраняет его навсегда.
|
||||
|
||||
Отвергнуто и названо ценой:
|
||||
|
||||
> сверяться с провайдером по расписанию — новая связь с Authelia и обработка её
|
||||
> недоступности, работа шире задачи; принять как есть — тогда паспорт теряет
|
||||
> способ остановить того, кто тратит слишком много.
|
||||
|
||||
Число семь суток выбрано владельцем как компромисс: реже входить против дольше
|
||||
ждать, пока отзыв доедет.
|
||||
|
||||
Срок назначается **при подъёме, а не шагом схемы**, и это отдельное решение с
|
||||
причиной: применённый шаг не переписывается, поэтому число, положенное туда,
|
||||
разошлось бы со сроком жизни куки при первой же правке — браузер получил бы
|
||||
новый срок, а хранилище продолжило выдавать прежний.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` отзыв доступа у провайдера доходит до сервиса гарантированно, максимум за
|
||||
семь суток; без этого он не доходил вовсе.
|
||||
- `+` срок жизни сессии стал числом, которое кто-то выбрал, и правится он в одном
|
||||
месте вместе со сроком куки.
|
||||
- `−` человек перевходит раз в неделю, и это заметно: своей страницы у сервиса
|
||||
нет, так что вход начинается с перехода по адресу входа руками.
|
||||
- `−` семь суток — всё ещё окно, в которое отозванный доступ работает. Немедленно
|
||||
закрыть чужую сессию можно только руками в панели, обновив ключ токенов записи;
|
||||
своего адреса у этого нет.
|
||||
- `−` закрытие продления сделано слоем приложения, а не настройкой коллекции:
|
||||
библиотека выдаёт сессию продлеваемой всегда, и отключить это в ней нечем.
|
||||
Слой придётся помнить при всякой правке маршрутов.
|
||||
+5
-1
@@ -32,9 +32,13 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-12 | [Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла](ADR-2026-08-12-protected-file-behind-session.md) | |
|
||||
| 2026-08-12 | [Сессия живёт семь суток и не продлевает саму себя](ADR-2026-08-12-session-without-refresh.md) | |
|
||||
| 2026-08-12 | [Кого пускать в сервис, решает правило провайдера, а не сервис](ADR-2026-08-12-access-delegated-to-provider.md) | |
|
||||
| 2026-08-12 | [Код провайдера меняется на сессию вызовом собственного адреса хранилища внутри процесса](ADR-2026-08-12-oidc-exchange-via-own-route.md) | |
|
||||
| 2026-08-12 | [Спекой нормируется и инструмент сборки, а не только поведение сервиса](ADR-2026-08-12-spec-norms-build-toolchain.md) | |
|
||||
| 2026-08-12 | [Объявленную версию Go шаг гейта читает из репозитория, а не спрашивает у инструмента](ADR-2026-08-12-version-read-from-repo-not-from-tool.md) | |
|
||||
| 2026-08-12 | [Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале](ADR-2026-08-12-file-link-open-but-not-logged.md) | |
|
||||
| 2026-08-12 | [Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале](ADR-2026-08-12-file-link-open-but-not-logged.md) | заменено на [ADR-2026-08-12-protected-file-behind-session](ADR-2026-08-12-protected-file-behind-session.md) |
|
||||
| 2026-08-12 | [Каталог данных задаётся одним ключом `[storage] data_dir`](ADR-2026-08-12-single-data-dir-config-key.md) | |
|
||||
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |
|
||||
| 2026-08-11 | [Отказ, который решено не проверять, объявляется поимённо](ADR-2026-08-11-errcheck-check-blank.md) | |
|
||||
|
||||
Reference in New Issue
Block a user