вход переехал на доверенный заголовок 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
@@ -0,0 +1,125 @@
package migrations
import (
"errors"
"fmt"
"github.com/pocketbase/pocketbase/core"
)
// ProviderLoginField — колонка, в которой лежит ключ учётной записи: логин
// человека у провайдера, тот самый, которым его называет обратный прокси.
//
// Имя говорит о происхождении значения, а не о заголовке, которым оно приехало:
// заголовок — способ доставки и может смениться, а логин у провайдера — то, чем
// значение является. Колонка уезжает шагом схемы и потому не переименовывается.
const ProviderLoginField = "provider_login"
// providerLoginIndex — имя уникального индекса по ключу учётной записи.
const providerLoginIndex = "idx_users_provider_login"
// up202608220001 переводит узнавание пришедшего с протокола OIDC на логин,
// который называет доверенный источник.
//
// Три части, и каждая закрывает своё.
//
// Первая — ключ учётной записи. Прежде идентичность человека лежала в системной
// таблице внешних учётных записей библиотеки: её вела механика обмена кода, и
// правил её только владелец панели. Механика уходит, и ключу нужен свой дом —
// колонка с уникальным индексом. Почта ключом не годится: провайдер не обязан
// её приносить, человек её меняет, а первое обращение с чужим адресом досталось
// бы чужой записи.
//
// Вторая — необязательная почта. Умолчание библиотеки требует непустого адреса
// у всякой учётной записи; заголовка с почтой может не быть вовсе, а
// уникальность почты держится **частичным** индексом (`WHERE email != ”`),
// поэтому записи без почты уживаются друг с другом. Пароль остаётся
// обязательным при любом значении признака — ему ставится случайный, употребить
// его нельзя: вход по паролю у коллекции выключен прежним шагом.
//
// Третья — поверхность коллекции пользователей. Умолчание библиотеки открывает
// владельцу записи чтение, правку и удаление собственной строки, и до сих пор
// это ничему не мешало ровно потому, что до поверхности хранилища браузер с
// кукой не дотягивался: слой предъявления жил под корнем приложения. С
// узнаванием по заголовку такая защита перестаёт быть защитой, а ключ учётной
// записи лежит теперь обычной колонкой — то есть правка своей записи и есть
// захват чужого имени: поставил себе ключом чужой логин, и первое обращение
// настоящего его владельца попало бы в твою запись вместе со всем архивом.
// Правила снимаются в пустое, что у хранилища означает «только владелец
// панели»; наш код читает и заводит запись мимо правил, панель работает
// суперпользователем, своих экранов профиля сервис не заводит.
func up202608220001(app core.App) error {
users, err := app.FindCollectionByNameOrId(UsersCollection)
if err != nil {
return fmt.Errorf("failed to find users collection: %w", err)
}
users.Fields.Add(&core.TextField{
Name: ProviderLoginField,
// Предел тот же, что у имени в умолчании библиотеки: логин длиннее
// имени не бывает, а колонка без предела принимала бы килобайты,
// пришедшие заголовком.
Max: 255,
})
// Индекс **частичный** — ровно как соседний индекс почты у той же коллекции.
// Сплошной запретил бы вторую запись с пустым ключом, а такая заводится
// рукой в панели и остаётся у всякой базы, пережившей прежний вход: подъём
// на ней ронял бы накатку шага отказом уникальности, и сервис не стартовал
// бы вовсе. Пустым ключом при этом не узнаётся никто — это держит приём
// значения, а не индекс.
users.AddIndex(providerLoginIndex, true, ProviderLoginField, ProviderLoginField+" != ''")
email, ok := users.Fields.GetByName(core.FieldNameEmail).(*core.EmailField)
if !ok {
return errors.New("users collection has no email field")
}
email.Required = false
// Механика OIDC снимается целиком: настройки провайдера больше не приводятся
// к конфигу при подъёме, и обменивать код не на что.
users.OAuth2.Enabled = false
users.OAuth2.Providers = nil
// Наглухо все пять: заведение записи идёт нашим кодом, мимо правил.
users.ListRule = nil
users.ViewRule = nil
users.CreateRule = nil
users.UpdateRule = nil
users.DeleteRule = nil
if err := app.Save(users); err != nil {
return fmt.Errorf("failed to switch users collection to provider login: %w", err)
}
return nil
}
// down202608220001 убирает ключ учётной записи и возвращает обязательность
// почты.
//
// Правила доступа сюда не возвращаются намеренно — ни открытое создание записи,
// которое закрывал прежний шаг, ни открытая правка, которую закрывает этот.
// Откат, восстанавливающий их, оставил бы сервис хуже, чем он был: правка своей
// записи открыта только тому, кто узнан, а узнают теперь по колонке, которую эта
// правка и переписывает.
func down202608220001(app core.App) error {
users, err := app.FindCollectionByNameOrId(UsersCollection)
if err != nil {
return fmt.Errorf("failed to find users collection: %w", err)
}
users.RemoveIndex(providerLoginIndex)
users.Fields.RemoveByName(ProviderLoginField)
email, ok := users.Fields.GetByName(core.FieldNameEmail).(*core.EmailField)
if !ok {
return errors.New("users collection has no email field")
}
email.Required = true
if err := app.Save(users); err != nil {
return fmt.Errorf("failed to restore users collection: %w", err)
}
return nil
}
@@ -51,6 +51,7 @@ func init() {
pbmigrations.Register(up202608140002, down202608140002, "202608140002_record_centric_model.go")
pbmigrations.Register(up202608140003, down202608140003, "202608140003_owner_required.go")
pbmigrations.Register(up202608150001, down202608150001, "202608150001_record_contract_columns.go")
pbmigrations.Register(up202608220001, down202608220001, "202608220001_trusted_header_login.go")
}
func ptr[T any](v T) *T { return &v }