HTTP API закрыт за вход через OIDC у Authelia
- шаг схемы закрывает поверхность, которую хранилище приносит открытой: собственную регистрацию, вход по паролю и одноразовый код — без этого закрытие приёма обходилось двумя запросами - продление сессии выключено, срок семь суток: иначе отзыв доступа у провайдера до сервиса не доходит никогда - файл записи отдаётся вошедшему по токену файла — пересмотр ADR-2026-08-12-file-link-open-but-not-logged
This commit is contained in:
@@ -99,6 +99,77 @@ pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайн
|
||||
библиотечной сборке тоже: пустое приложение с одним своим обработчиком отвечало
|
||||
на `/_/` кодом `200`.
|
||||
|
||||
## Вход через OIDC: что выяснилось при реализации
|
||||
|
||||
Дописано 2026-08-12 задачей `oidc-login`. Провенанс общий: чтение исходников
|
||||
`pocketbase@v0.39.10` из кеша модулей плюс прогоны против настоящего хранилища на
|
||||
временном каталоге, все — в ходе ревью того change. Живой Authelia в прогонах не
|
||||
было ни разу: провайдера подменял свой `httptest`-сервер.
|
||||
|
||||
**Коллекция `users` приходит открытой.** Системный шаг библиотеки заводит её с
|
||||
`CreateRule = ""` (создание доступно анониму) и `PasswordAuth.Enabled = true`
|
||||
(`migrations/1640988000_init.go`, `core/collection_model_auth_options.go`).
|
||||
Прогон подтвердил: `POST /api/collections/users/records` → `200`, следом
|
||||
`auth-with-password` → `200` с токеном. То есть закрытие API за вход обходится
|
||||
двумя запросами, пока эта поверхность не закрыта своим шагом схемы.
|
||||
|
||||
**Правило создания нельзя закрывать полностью.** `CreateRule = nil` означает «только
|
||||
суперпользователь», а запись при первом входе заводит **внутренний** запрос
|
||||
самого обмена, идущий без таких прав (`apis/record_crud.go`: проверка
|
||||
`!hasSuperuserAuth && collection.CreateRule == nil`). Прогон: с `nil` вход
|
||||
кончался `401`, учётных записей `0`. Работает правило
|
||||
`@request.context = "oauth2"` — контекст ставит сам обмен
|
||||
(`core.RequestInfoContextOAuth2`), а посторонний запрос приходит с контекстом по
|
||||
умолчанию. Открывать правило пустой строкой при этом нельзя: публичный обмен
|
||||
принимает поля создаваемой записи от вызывающего.
|
||||
|
||||
**Обмен кода наружу не экспортирован.** Пакет `apis` отдаёт ошибки, middleware,
|
||||
`NewRouter`, `Serve` и обёртки; сам обмен — неэкспортированная функция за
|
||||
маршрутом `POST /api/collections/{c}/auth-with-oauth2`, принимающая `provider`,
|
||||
`code`, `codeVerifier`, `redirectURL`. Собственный `/api/oauth2-redirect` служит
|
||||
другому — он ищет клиента realtime-подписки по параметру `state`, то есть
|
||||
обслуживает всплывающее окно JS-клиента, а не серверный вход.
|
||||
|
||||
**`apis.NewRouter` не идемпотентна: собирать её нужно один раз и держать, а не
|
||||
создавать заново при каждом вызове.**
|
||||
Она зовёт `bindRealtimeEvents` и `bindUIExtensions`, а те вешают девять
|
||||
обработчиков **на приложение** и без поля `Id`; `hook.Bind` такому генерирует
|
||||
новый идентификатор и **добавляет**. Замер: пять вызовов подряд подняли
|
||||
`OnModelAfterUpdateSuccess` с 4 до 14, а 3000 вызовов — время сотни сохранений
|
||||
записи с 3.86 мс до 59.8 мс и кучу на 5013 КиБ. Освобождения нет, только
|
||||
перезапуск.
|
||||
|
||||
**Связывание учётной записи идёт по `sub`, а не найдя — по почте.** Обмен ищет
|
||||
запись в `_externalAuths` по `providerId`, и лишь затем `FindAuthRecordByEmail`
|
||||
(`apis/record_auth_with_oauth2.go`). Отсюда цена открытой регистрации: запись,
|
||||
заведённая посторонним на чужой адрес почты, достаётся первому же настоящему
|
||||
входу с этим адресом.
|
||||
|
||||
**Защищённое поле файла судится двумя вещами сразу** — коротким токеном файла из
|
||||
строки запроса **и** правилом просмотра коллекции (`apis/file.go`). Незаданное
|
||||
правило означает «только суперпользователь», поэтому одной пометки `Protected`
|
||||
мало: прогон показал `404` анониму, вошедшему кукой, вошедшему заголовком и
|
||||
вошедшему с законно полученным токеном файла — пока правило не назначено.
|
||||
|
||||
**Сессия по умолчанию продлеваема бессрочно.** Токен несёт поле
|
||||
`refreshable=true`, и `POST /api/collections/{c}/auth-refresh` меняет его на
|
||||
новый с новым сроком. Прогон: три продления подряд, каждое `200`, `exp` растёт.
|
||||
Настройки «выдавать непродлеваемую сессию» у коллекции нет — закрывается только
|
||||
слоем приложения поверх маршрута.
|
||||
|
||||
**Подпись сессии считается от секрета коллекции и ключа записи**, обе величины в
|
||||
базе (`core/record_query.go`, `FindAuthRecordByToken`). Отсюда два следствия:
|
||||
сессия переживает перезапуск сервиса сама, а смена ключа записи
|
||||
(`Record.RefreshTokenKey()`) обесценивает все её выданные сессии разом.
|
||||
|
||||
**Куки библиотека не читает вовсе** — сессию берёт только заголовком
|
||||
`Authorization` (`apis/middlewares.go`, `getAuthTokenFromRequest`).
|
||||
|
||||
**Журнал запросов пишет строку запроса целиком.** `activityLogger` на корневом
|
||||
роутере кладёт `RequestURI` полем `url` в таблицу `_logs`, ретеншен по умолчанию
|
||||
`MaxDays: 5`. Значит всё, что пришло параметром адреса, оседает там на пять
|
||||
суток; проект умолчание не переопределяет.
|
||||
|
||||
## Что отвергнуто и почему
|
||||
|
||||
- **Держать файлы на диске как сейчас, а в базе — путь строкой.** Отвергнуто:
|
||||
|
||||
Reference in New Issue
Block a user