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 }