- шаги PocketBase переехали из файла в пакет internal/adapter/repo/pocketbase/migrations, файл на шаг с именем зарегистрированного шага; туда же имена коллекций, срок сессии — в provider.go - ключ migrations в docs/.docs.json наведён на этот каталог: прежнее значение указывало на несуществующий migrations/, и шаг гейта проходил зелёным при всякой правке схемы - app.go подключает пакет шагов явным пустым импортом: пропавшая ссылка на константы унесла бы регистрацию, и хранилище поднялось бы без коллекций
125 lines
6.6 KiB
Go
125 lines
6.6 KiB
Go
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
|
|
}
|