package migrations import ( "errors" "fmt" "github.com/pocketbase/pocketbase/core" ) // defaultAuthTokenDuration — умолчание библиотеки, к которому возвращает откат. const defaultAuthTokenDuration = 1209600 // up202608120001 закрывает поверхность, которую хранилище приносит своим // системным шагом, и защищает файл записи. // // Коллекция пользователей заводится библиотекой с открытым созданием записи и // включённым входом по паролю. Без этого шага закрытие API обходится двумя // запросами: завести себе учётную запись, войти паролем, предъявить полученное // заголовком. Отдельная цена открытого создания — захват учётной записи: обмен // кода ищет запись сперва по неизменяемому признаку провайдера, а не найдя — // по адресу почты, и запись, заведённая посторонним на чужой адрес, достаётся // первому же настоящему входу с этим адресом. func up202608120001(app core.App) error { users, err := app.FindCollectionByNameOrId("users") if err != nil { return fmt.Errorf("failed to find users collection: %w", err) } // Завести учётную запись можно только входом у провайдера. // // Правило именно такое, а не `nil`: запись при первом входе заводит // внутренний запрос самого обмена, и он идёт без прав суперпользователя — // глухое `nil` отвергло бы его наравне с посторонним, и войти не смог бы // никто. Контекст `oauth2` ставит обмен (`core.RequestInfoContextOAuth2`), // а посторонний запрос приходит с контекстом по умолчанию. // // Открывать правило пустой строкой нельзя: публичный обмен принимает поля // создаваемой записи от вызывающего, и всякий владелец учётной записи у // провайдера задал бы их сам. users.CreateRule = ptr(`@request.context = "oauth2"`) users.PasswordAuth.Enabled = false users.OTP.Enabled = false // Провайдер включается здесь с пустыми значениями: адреса, идентификатор // клиента и секрет приходят из конфига при каждом подъёме. Положенный сюда // секрет не пережил бы ротации — применённый шаг не переписывается. users.OAuth2.Enabled = true if err := app.Save(users); err != nil { return fmt.Errorf("failed to close users collection surface: %w", err) } files, err := app.FindCollectionByNameOrId(FilesCollection) if err != nil { return fmt.Errorf("failed to find files collection: %w", err) } // Ссылка на файл перестаёт быть правом пройти по ней: до этого шага знание // ссылки и было доступом, а отзыва у неё нет. Конвейер этим не затронут — // он читает файл из файловой системы хранилища, а не по ссылке. // // Комментарий прежнего шага утверждает обратное — «защищённым поле не // помечено намеренно». Прежний шаг не переписывается, поэтому решение // отменяется здесь: право прочитать запись больше не даёт знание её // идентификатора. field, ok := files.Fields.GetByName("file").(*core.FileField) if !ok { return errors.New("files collection has no file field") } field.Protected = true // Одной пометки мало: защищённый файл судится ещё и правилом просмотра // коллекции, а незаданное правило означает «только владелец панели» — файл // не получил бы и вошедший. Правило пускает всякого узнанного: владельца у // записи ещё нет, и сужать выборку эта задача не должна. files.ViewRule = ptr(`@request.auth.id != ""`) if err := app.Save(files); err != nil { return fmt.Errorf("failed to protect record file: %w", err) } return nil } // down202608120001 возвращает умолчания библиотеки — те, что стояли до шага. // // Открытое создание записи сюда не возвращается намеренно: это ровно то, что // шаг и закрывал, и откат, восстанавливающий анонимную регистрацию, оставил бы // сервис хуже, чем он был до задачи. Срок жизни сессии возвращается // умолчанием, а не нулём: нулевую длительность валидация коллекции отвергает, и // прежний откат падал на ней, не дойдя до снятия защиты с файла. func down202608120001(app core.App) error { users, err := app.FindCollectionByNameOrId("users") if err != nil { return fmt.Errorf("failed to find users collection: %w", err) } users.CreateRule = nil users.PasswordAuth.Enabled = true users.OTP.Enabled = true users.OAuth2.Enabled = false users.OAuth2.Providers = nil users.AuthToken.Duration = defaultAuthTokenDuration if err := app.Save(users); err != nil { return fmt.Errorf("failed to restore users collection: %w", err) } files, err := app.FindCollectionByNameOrId(FilesCollection) if err != nil { return fmt.Errorf("failed to find files collection: %w", err) } if field, ok := files.Fields.GetByName("file").(*core.FileField); ok { field.Protected = false } files.ViewRule = nil if err := app.Save(files); err != nil { return fmt.Errorf("failed to unprotect record file: %w", err) } return nil }