у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// up202608140001 заводит владельца записи.
|
||||
//
|
||||
// Колонка — связь с коллекцией пользователей: хранилище само следит, чтобы
|
||||
// владельцем стояла существующая учётная запись, а не строка, похожая на её
|
||||
// идентификатор.
|
||||
//
|
||||
// Пустое значение допустимо, и это решение с названной ценой. Записи, принятые
|
||||
// ботом, владельца не имеют вовсе: связи чата Telegram с учётной записью сервис
|
||||
// не ведёт, её заводит отдельная задача. Обязательность для приёма по HTTP
|
||||
// держит поэтому сам приём, а не схема.
|
||||
//
|
||||
// Каскадное удаление выключено, но одного этого мало: при выключенном каскаде
|
||||
// хранилище **вынимает** идентификатор из поля связи и сохраняет запись без
|
||||
// проверок, то есть архив удалённого пользователя стал бы ничьим и не достался
|
||||
// бы никому. Поэтому удаление учётной записи, у которой остались задачи,
|
||||
// отвергается слоем приложения — `GuardOwnerDeletion`.
|
||||
func up202608140001(app core.App) error {
|
||||
users, err := app.FindCollectionByNameOrId(UsersCollection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find users collection: %w", err)
|
||||
}
|
||||
|
||||
jobs, err := app.FindCollectionByNameOrId(JobsCollection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find jobs collection: %w", err)
|
||||
}
|
||||
|
||||
jobs.Fields.Add(ownerField(users.Id))
|
||||
|
||||
if err := app.Save(jobs); err != nil {
|
||||
return fmt.Errorf("failed to add owner to jobs: %w", err)
|
||||
}
|
||||
|
||||
files, err := app.FindCollectionByNameOrId(FilesCollection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find files collection: %w", err)
|
||||
}
|
||||
|
||||
files.Fields.Add(ownerField(users.Id))
|
||||
|
||||
// Правило просмотра сужается владельцем. Прежнее пускало всякого узнанного:
|
||||
// владельца у записи тогда не было, и сужать выборку было нечем. Без этой
|
||||
// строки разграничение закрыло бы метаданные задачи и оставило открытым
|
||||
// содержимое — то самое, что оно и заведено прятать: знание идентификатора
|
||||
// файловой записи равнялось бы праву скачать чужое аудио.
|
||||
files.ViewRule = ptr(`@request.auth.id != "" && owner = @request.auth.id`)
|
||||
|
||||
if err := app.Save(files); err != nil {
|
||||
return fmt.Errorf("failed to narrow files by owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ownerField собирает описание колонки владельца. Обе коллекции получают
|
||||
// одинаковую: разойдясь, они дали бы разное поведение у задачи и у её файла.
|
||||
func ownerField(usersCollectionID string) *core.RelationField {
|
||||
return &core.RelationField{
|
||||
Name: "owner",
|
||||
CollectionId: usersCollectionID,
|
||||
MaxSelect: 1,
|
||||
// Пустое значение допустимо — см. шапку шага. Умолчания у колонки нет:
|
||||
// связь его не имеет по устройству, и запись не достаётся никому по
|
||||
// недосмотру схемы.
|
||||
Required: false,
|
||||
// Удаление учётной записи не уносит её записи следом: сервис объявлен
|
||||
// архивом. Что происходит вместо этого, держит `GuardOwnerDeletion`.
|
||||
CascadeDelete: false,
|
||||
}
|
||||
}
|
||||
|
||||
// down202608140001 снимает колонку с обеих коллекций и возвращает правило
|
||||
// просмотра файлов к тому, что стояло до шага, — «всякий узнанный».
|
||||
func down202608140001(app core.App) error {
|
||||
for _, name := range []string{JobsCollection, FilesCollection} {
|
||||
collection, err := app.FindCollectionByNameOrId(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find collection %s: %w", name, err)
|
||||
}
|
||||
|
||||
field := collection.Fields.GetByName("owner")
|
||||
if field == nil {
|
||||
return errors.New("collection " + name + " has no owner field")
|
||||
}
|
||||
collection.Fields.RemoveById(field.GetId())
|
||||
|
||||
if name == FilesCollection {
|
||||
collection.ViewRule = ptr(`@request.auth.id != ""`)
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return fmt.Errorf("failed to drop owner from %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -24,6 +24,10 @@ import (
|
||||
const (
|
||||
FilesCollection = "files"
|
||||
JobsCollection = "transcribe_jobs"
|
||||
// UsersCollection заводит не наш шаг, а системный шаг библиотеки. Имя стоит
|
||||
// здесь потому, что на него ссылаются и шаги схемы, и проверка предъявителя
|
||||
// на приёме: строковый литерал в двух местах разошёлся бы молча.
|
||||
UsersCollection = "users"
|
||||
)
|
||||
|
||||
// Шаг регистрируется в списке приложения при загрузке пакета, а накатывает его
|
||||
@@ -31,6 +35,7 @@ const (
|
||||
func init() {
|
||||
pbmigrations.Register(up202608110001, down202608110001, "202608110001_init.go")
|
||||
pbmigrations.Register(up202608120001, down202608120001, "202608120001_oidc_login.go")
|
||||
pbmigrations.Register(up202608140001, down202608140001, "202608140001_record_owner.go")
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
Reference in New Issue
Block a user