Права доступа в PocketBase: как настроить API Rules

Обновлено:

API Rules — механизм контроля доступа в PocketBase. Для каждой коллекции вы задаёте выражения-фильтры для пяти операций: list, view, create, update и delete. Правила проверяются на сервере: если выражение не выполняется, запись не попадёт в выдачу или операция будет отклонена. Если вы только знакомитесь с PocketBase, начните со статьи что такое PocketBase.

Синтаксис API Rules: основы

Есть три состояния правила:

  • null (правило заблокировано, в панели — замок) — операция доступна только superuser;
  • пустая строка "" — операция доступна всем, включая неавторизованных;
  • выражение — операция доступна, если выражение выполняется.
# Открыто всем
listRule: ""

# Только superuser
listRule: null

# Только авторизованные
listRule: @request.auth.id != ""

# Только владелец
listRule: user = @request.auth.id

Не путайте null и "": это противоположные по смыслу значения.

Доступные переменные

@request.auth — авторизованный пользователь:

  • @request.auth.id — ID (пустая строка, если не авторизован);
  • @request.auth.email, @request.auth.verified;
  • @request.auth.collectionName — имя auth-коллекции;
  • @request.auth.<поле> — любое поле записи пользователя.

@request.body.<поле> — значение поля из тела запроса (create/update).

@request.query.<параметр>, @request.headers.<заголовок>, @request.method — параметры запроса.

@collection.<коллекция>.<поле> — обращение к другой коллекции, даже если прямой связи нет.

@now — текущее время (UTC).

Операторы сравнения

=    равно
!=   не равно
>    больше
>=   больше или равно
<    меньше
<=   меньше или равно
~    содержит (LIKE '%value%')
!~   не содержит
?=   хотя бы один элемент равен
?!=  хотя бы один элемент не равен
?~   хотя бы один элемент содержит

Операторы с ? нужны для полей с несколькими значениями: multiple relation, multiple select, обратные связи. Без ? условие должно выполняться для всех элементов.

Логические операторы

&&   И
||   ИЛИ
()   группировка

Паттерн 1: Публичная коллекция (блог, каталог)

# Коллекция: articles

listRule:   published = true
viewRule:   published = true || author = @request.auth.id
createRule: @request.auth.id != "" && @request.body.author = @request.auth.id
updateRule: author = @request.auth.id
deleteRule: author = @request.auth.id
  • список: всем видны только опубликованные статьи;
  • просмотр: опубликованные — всем, черновики — автору;
  • создание: только авторизованные и только от своего имени;
  • изменение и удаление: только автор.

Паттерн 2: Личная коллекция

Пользователь видит и меняет только свои записи:

# Коллекция: notes

listRule:   owner = @request.auth.id
viewRule:   owner = @request.auth.id
createRule: @request.auth.id != "" && @request.body.owner = @request.auth.id
updateRule: owner = @request.auth.id &&
            (@request.body.owner:isset = false || @request.body.owner = @request.auth.id)
deleteRule: owner = @request.auth.id

Проверка @request.body.owner в createRule не даёт создать запись от имени другого пользователя. В updateRule модификатор :isset запрещает «передать» запись чужому владельцу.

Паттерн 3: Командный доступ (SaaS с ролями)

Структура: коллекция team_members с полями team, user, role (admin, member, viewer). У самой teams нет поля со списком участников, поэтому членство проверяется через @collection.team_members или обратную связь team_members_via_team.

# Коллекция: projects (поле team → teams)

listRule:   @collection.team_members.team ?= team &&
            @collection.team_members.user ?= @request.auth.id
viewRule:   @collection.team_members.team ?= team &&
            @collection.team_members.user ?= @request.auth.id
createRule: @collection.team_members.team ?= @request.body.team &&
            @collection.team_members.user ?= @request.auth.id &&
            @collection.team_members.role ?!= "viewer"
updateRule: @collection.team_members.team ?= team &&
            @collection.team_members.user ?= @request.auth.id &&
            @collection.team_members.role ?!= "viewer"
deleteRule: team.owner = @request.auth.id ||
            (@collection.team_members.team ?= team &&
             @collection.team_members.user ?= @request.auth.id &&
             @collection.team_members.role ?= "admin")

Главная ловушка. Проверки team, user и role должны сработать для одной и той же записи членства. Правило вида team.members.role ?!= "viewer", где роль проверяется отдельно от пользователя, означает «в команде есть хоть кто-то не-viewer» — и тогда viewer получает права на запись. Каждое командное правило проверяйте на тестовых пользователях: admin, member и viewer в двух разных командах. Если правило становится нечитаемым, перенесите проверку роли в хук (пример ниже).

Паттерн 4: Публичное чтение, запись для авторизованных

Форумы, комментарии, отзывы. Предполагается, что в users есть поле role:

# Коллекция: comments

listRule:   ""
viewRule:   ""
createRule: @request.auth.id != "" && @request.body.author = @request.auth.id
updateRule: author = @request.auth.id
deleteRule: author = @request.auth.id || @request.auth.role = "moderator"

Паттерн 5: Только подтверждённые пользователи

# Коллекция: premium_content

listRule: @request.auth.id != "" && @request.auth.verified = true
viewRule: @request.auth.id != "" && @request.auth.verified = true

Create, update и delete для такого контента обычно оставляют заблокированными (null) — их выполняет superuser или серверный код.

Паттерн 6: Доступ до определённой даты

# Коллекция: temp_access

listRule: user = @request.auth.id && expires_at >= @now
viewRule: user = @request.auth.id && expires_at >= @now

Паттерн 7: Роли в коллекции users

Если роль хранится в самой записи пользователя (role: Select [user, moderator, admin]):

# Коллекция: reports

listRule:   @request.auth.id != ""
viewRule:   owner = @request.auth.id || @request.auth.role = "admin"
createRule: @request.auth.id != ""
updateRule: owner = @request.auth.id || @request.auth.role = "admin"
deleteRule: @request.auth.role = "admin"

Используйте =, а не ~: @request.auth.role ~ "admin" совпадёт и с ролью superadmin. И запретите пользователю менять собственную роль: в updateRule коллекции users добавьте @request.body.role:isset = false.

Superuser обходит все правила

Запросы от superuser игнорируют API Rules. Поэтому учётные данные superuser нельзя отправлять в браузер — только серверный код.

// Неправильно: токен superuser в браузере
const pb = new PocketBase('https://myapp.ru')
pb.authStore.save(SUPERUSER_TOKEN, null) // никогда так не делайте

// Правильно: только на сервере (Route Handler, Server Action, воркер)
const adminPb = new PocketBase(process.env.PB_URL!)
await adminPb.collection('_superusers').authWithPassword(
  process.env.PB_SUPERUSER_EMAIL!,
  process.env.PB_SUPERUSER_PASSWORD!
)
const allUsers = await adminPb.collection('users').getFullList()

В старых версиях PocketBase для этого был pb.admins.authWithPassword; в актуальных администраторы — это записи системной коллекции _superusers.

Отладка API Rules

Запустите PocketBase в режиме разработки — он будет печатать выполняемые SQL-запросы, по ним видно, во что превратилось правило:

./pocketbase serve --dev

Проверяйте правила запросами от разных пользователей:

# Без авторизации
curl http://localhost:8090/api/collections/posts/records

# С авторизацией
TOKEN=$(curl -s -X POST http://localhost:8090/api/collections/users/auth-with-password \
  -H "Content-Type: application/json" \
  -d '{"identity":"test@example.com","password":"password123"}' | jq -r '.token')

curl http://localhost:8090/api/collections/posts/records \
  -H "Authorization: $TOKEN"

Обратите внимание: если listRule не выполняется, PocketBase не возвращает ошибку, а просто отдаёт пустой список. Для view/update/delete недоступная запись выглядит как 404.

Сложные проверки через хуки

Лимиты по тарифу, подсчёты и проверки, которые не выражаются фильтром, переносите в хуки:

// pb_hooks/check_quota.pb.js

onRecordCreateRequest((e) => {
    const auth = e.auth
    if (!auth) {
        throw new UnauthorizedError('Требуется авторизация')
    }

    const count = $app.countRecords(
        'projects',
        $dbx.exp('owner = {:userId}', { userId: auth.id })
    )
    const maxProjects = auth.getString('plan') === 'pro' ? 50 : 5

    if (count >= maxProjects) {
        throw new ForbiddenError(`Достигнут лимит проектов (${maxProjects}) для вашего тарифа`)
    }

    e.next()
}, 'projects')

Перед изменением правил — бэкап pb_data

Правила и схема коллекций хранятся в pb_data/data.db (а при включённых автомиграциях — ещё и в pb_migrations). Ошибочная миграция или неудачная правка схемы может удалить поле вместе с данными. Перед такими изменениями сделайте копию базы — например, встроенным бэкапом в панели или sqlite3 pb_data/data.db ".backup 'before-rules.db'". Автоматическая схема с версиями и проверкой восстановления описана в статье бэкап PocketBase.

Частые вопросы

Как разрешить операцию только superuser?

Оставьте правило заблокированным (null), а не пустой строкой: "" открывает доступ всем. Для частичных прав без superuser добавьте в users поле is_admin (Bool) и проверяйте @request.auth.is_admin = true, запретив пользователям менять это поле.

Почему для связей нужен ?=, а не =?

Для полей с несколькими значениями = означает «все элементы равны», а ?= — «хотя бы один равен». Для проверки членства почти всегда нужен ?=. Для одиночной relation, которая хранит один ID, работает обычный =.

Можно ли скрыть поля на уровне сервера?

Параметр ?fields= выбирает клиент, поэтому он не защищает данные. На сервере отметьте поле как скрытое (Hidden) в настройках коллекции — тогда его увидит только superuser — или меняйте ответ в хуке onRecordEnrich. Другой вариант — отдельная view-коллекция с нужными полями.

Как проверить правила без клиентского кода?

В панели у каждой коллекции есть API Preview: он показывает эндпоинты, параметры и действующие правила, но запросы не выполняет. Сами проверки удобно делать через curl, Postman или небольшой скрипт на SDK от имени разных тестовых пользователей.

Работают ли API Rules для realtime-подписок?

Да. При подписке на всю коллекцию (subscribe('*')) применяется listRule, при подписке на конкретную запись — viewRule. Пользователь получает события только по записям, которые ему разрешено видеть.