вход переехал на доверенный заголовок 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
+45
View File
@@ -1,6 +1,8 @@
package pocketbase
import (
"errors"
"github.com/pocketbase/pocketbase/core"
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
@@ -31,6 +33,8 @@ import (
// бы тем же сохранением, а число отказов остановленной записи — которое
// остановка хранит намеренно — приходило бы владельцу нулём.
func BindPanelRules(app core.App) {
bindProviderLoginIsImmutable(app)
app.OnRecordUpdateRequest(migrations.RecordsCollection).BindFunc(func(e *core.RecordRequestEvent) error {
original := e.Record.Original()
if original == nil {
@@ -84,3 +88,44 @@ func BindPanelRules(app core.App) {
return nil
})
}
// bindProviderLoginIsImmutable запрещает менять ключ учётной записи после
// заведения.
//
// Ключ — логин человека у провайдера, и по нему сервис узнаёт пришедшего.
// Переписанный, он отдаёт весь архив прежнего владельца следующему, кто придёт
// с этим именем: владелец записи назначается один раз и не меняется, так что
// вернуть архив будет нечем. Молча — журнала событий у коллекции пользователей
// нет.
//
// Правила доступа коллекции закрывают этот путь **снаружи**, но не изнутри:
// панель работает суперпользователем и правила обходит по построению. Отсюда
// хук, и он вешается на **модельное** событие, а не на правку запросом — иначе
// панель осталась бы незакрытой, а закрывать её и есть весь смысл.
//
// Заведение проходит: событие правки на нём не срабатывает вовсе.
//
// Прежнее значение читается **из базы**, а не из снимка правящейся записи.
// Снимок у записи, только что заведённой в этом же процессе, пуст — он не
// обновляется сохранением, — и сторож, опирающийся на него, пропускал бы правку
// в зависимости от того, откуда вызывающий взял запись. Панель её загружает, и
// на ней сторож сработал бы; молчаливая же зависимость от способа получения —
// ровно тот класс, из-за которого правило и заводится.
func bindProviderLoginIsImmutable(app core.App) {
app.OnRecordUpdate(migrations.UsersCollection).BindFunc(func(e *core.RecordEvent) error {
stored, err := e.App.FindRecordById(migrations.UsersCollection, e.Record.Id)
if err != nil {
// Записи в базе нет — правки тоже нет: сохранение отвергнется само.
return e.Next()
}
was := stored.GetString(migrations.ProviderLoginField)
now := e.Record.GetString(migrations.ProviderLoginField)
if was != "" && was != now {
return errors.New("provider login is assigned once and never changes")
}
return e.Next()
})
}