вход переехал на доверенный заголовок Authelia вместо собственного OIDC

- пришедшего называет заголовок Remote-User от прокси, и верят ему только с
  адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни
  корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе
- учётная запись заводится первым обращением: EnsureUser в пакете хранилища,
  шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users
- cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт
  унаследованный DL3066 — пользователь образа назван числом
This commit is contained in:
av
2026-08-22 20:24:22 +03:00
parent e4441f3c49
commit 7f33c957e5
63 changed files with 5257 additions and 2305 deletions
+39 -10
View File
@@ -271,15 +271,33 @@ capability, и третий смысл развёл бы одно слово п
- **Файл отдаётся ссылкой** `/api/files/<коллекция>/<запись>/<имя>`. Поле файла
помечено защищённым шагом `202608120001`, а правило просмотра коллекции
пускает всякого вошедшего: пройти по ссылке можно только с коротким токеном
файла, который берут по сессии. Прежнее решение — «право прочитать запись даёт
файла, который берёт узнанный. Прежнее решение — «право прочитать запись даёт
знание её идентификатора» — отменено задачей `oidc-login` 2026-08-12. Имя файла
в хранилище **в журнал не пишется** по-прежнему: оно последняя часть ссылки.
- **Коллекция `users`** заводится самой библиотекой, а шаг `202608120001` её
сужает: создание записи разрешено только контексту обмена OIDC
(`@request.context = "oauth2"`), вход по паролю и одноразовый код выключены.
Без этого сужения закрытие API обходится двумя запросами — завести себе
запись и войти паролем. Продление сессии закрыто слоем в приложении, а не
настройкой коллекции: библиотека выдаёт сессию продлеваемой всегда.
- **Коллекция `users`** заводится самой библиотекой, а два наших шага её сужают.
`202608120001` выключил вход по паролю и одноразовый код; `202608220001`
довершил: снял настройки OAuth2 и **все пять правил доступа** — перечисление,
чтение, создание, правку и удаление, — оставив их пустыми, что у хранилища
означает «только владелец панели».
Правку и удаление умолчание библиотеки открывало владельцу записи
(`id = @request.auth.id`), и до переезда входа это ничему не мешало: слой
предъявления жил под корнем приложения, и браузер до поверхности хранилища не
дотягивался. С узнаванием по заголовку она достижима, а ключ учётной записи
лежит теперь обычной колонкой — правка своей записи была бы присвоением чужого
имени. Наш код читает и заводит запись мимо правил, панель работает
суперпользователем, своих экранов профиля сервис не заводит.
- **Ключ учётной записи — колонка `provider_login`** с уникальным индексом,
заведена шагом `202608220001`. В ней логин человека **у провайдера** — то
значение, которым его называет обратный прокси заголовком. По нему запись
ищется и по нему же заводится при первом обращении.
Почта в той же коллекции переведена в необязательную тем же шагом: провайдер
не обязан её приносить, а ключом она не служит. Уникальность почты держится
**частичным** индексом (`WHERE email != ''`), поэтому записи без почты
уживаются друг с другом; уникальность логина — обычным, поэтому двух записей с
пустым ключом схема не примет вовсе.
- **Захват записи — один запрос с `RETURNING`**, мимо записей коллекции.
`app.DB()` направляет всё, кроме выборок, в пул с единственным соединением,
поэтому захваты выстраиваются в очередь. Порядок выборки — по времени
@@ -324,6 +342,14 @@ capability, и третий смысл развёл бы одно слово п
| Умолчание размера страницы списка | 30 | `controller/http.DefaultPageLimit` | столько помещается на экран телефона без прокрутки в два экрана |
| Потолок размера страницы списка | 100 | `controller/http.MaxPageLimit` | против того, чтобы попросить весь архив одним запросом и тем обойти постраничность её же параметром |
| Ограничитель частоты под `/app/` | 120 запросов за 60 секунд | `controller/http.appRateMaxRequests`, `appRateWindowSec` | сервисом пользуются единицы человек; бюджет считается по адресу спрашивающего, а не по учётной записи |
**Адрес спрашивающего берётся из `X-Forwarded-For`, и это назначается кодом при
подъёме** — `controller/http.ApplyTrustedProxyHeaders`. Без этого хранилище
ключует счётчик адресом пира, а пир с переездом входа на заголовок всегда один и
тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и восемь
одновременно открытых карточек выбирают его целиком. Требование к контуру,
которое отсюда следует, записано в [security.md](security.md), «Периметр»:
`X-Forwarded-For` прокси обязан перезаписывать, а не дописывать.
| Доля бюджета под опрос карточки | 1/8 | `controller/http.pollBudgetShare` | опрос идёт не один: в ту же секунду приложение листает список и грузит новую запись. Из этой доли **выводится** объявляемая частота опроса, и своей константы у неё нет |
| Потолок длины имени файла отправителя | 255 знаков | `entity.MaxOriginalFilenameLen` | предел длины имени в распространённых файловых системах: длиннее системный диалог выбора файла не даёт |
| Потолок длины расширения | 32 знака | `service/transcribe.go`, `maxExtLen` | сторож от патологии, а не перечень: расширения известных форматов укладываются в пять знаков, а `x.` с четырьмястами знаками роняет заведение временного файла |
@@ -339,9 +365,12 @@ capability, и третий смысл развёл бы одно слово п
| Качество кодирования vorbis | `-q:a 4` | `adapter/converter/ffmpeg/ffmpeg.go` | — |
| Жизнь приглашения завести владельца панели | 30 минут | умолчание PocketBase | — |
| Потолок размера одной записи | 8 ГиБ | `entity.MaxRecordSize` | расчётный потолок в шесть часов с запасом на видео |
| Срок жизни сессии | нормирует [access](../openspec/specs/access/spec.md) | `pbrepo.SessionDuration`, ставится при подъёме | решение владельца 2026-08-12; умолчание библиотеки никем не выбрано, и спека прямо запрещает его применять |
| Потолок времени на вход у провайдера | 10 минут | `controller/http/auth.go` | дольше носитель состояния не нужен |
| Таймаут обмена кода у провайдера | 15 секунд | там же | молчащий провайдер иначе держит обработчик возврата открытым |
| Предел длины логина у провайдера | 255 знаков | `pbrepo.MaxProviderLoginLength` и колонка `provider_login` | значение приходит заголовком, то есть задаётся тем, кто шлёт запрос; число то же, что у имени в умолчании библиотеки |
Три числа отсюда ушли 2026-08-22 вместе с собственным входом: срок жизни сессии,
потолок времени на вход у провайдера и таймаут обмена кода. Сессия не выдаётся
вовсе, обменивать код не на что, а отзыв доступа судит провайдер на каждом
запросе — задержке, которую измерял срок сессии, теперь неоткуда взяться.
**У сторожа простоя есть второй потолок, и он не тот, что в настройке.** Предел
простоя проверяется в момент захвата, а захват не выдаёт запись, чей срок