- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
126 lines
7.7 KiB
Go
126 lines
7.7 KiB
Go
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
|
|
}
|