Перейти к содержанию

Роли и правила

Система прав FARA устроена в три слоя:

  1. Роли (Role) — что-то вроде «должности»: набор разрешений, который можно выдать пользователю.
  2. ACL (AccessList) — права на уровне таблицы: может ли роль читать/создавать/изменять/удалять записи модели в принципе.
  3. Rules (Rule) — правила на уровне строк: какие именно записи модели видит/изменяет роль.

ACL отвечает на вопрос «может ли пользователь работать с этой таблицей вообще», Rules — «с какими записями этой таблицы».

Архитектура

graph LR
    U[User] -->|user_role_many2many| R[Role]
    R -->|based_role_ids| R2[Role]
    R --> ACL[AccessList<br/>perm_create, perm_read,<br/>perm_update, perm_delete]
    R --> RU[Rule<br/>domain — JSON]
    ACL --> M[Model]
    RU --> M

    style R fill:#dde7ff,stroke:#5170c4
    style ACL fill:#d1f7c4,stroke:#2c6c1c
    style RU fill:#fde6c4,stroke:#8b4f1c

Модель Role

code Char(64) unique

Машинное имя роли — base_user, crm_manager, viewer. По нему роль ищется в коде.

name Char(128)

Человекочитаемое название для UI.

based_role_ids Many2many<Role>

Иерархия: «эта роль наследует права от...». Если crm_manager.based_role_ids = [base_user] — менеджер автоматически получает все права base_user плюс свои собственные.

acl_ids One2many<AccessList>

Список ACL — прав на конкретные модели. Один ACL = одна модель + флаги CRUD.

rule_ids One2many<Rule>

Список правил — domain-фильтров на записи.

Получение всех ролей пользователя с учётом иерархии

all_roles = await Role.get_all_roles([user_role_id])
# Возвращает [user_role_id] + все based_role_ids рекурсивно.
# Использует рекурсивный CTE — один запрос вместо N+1.

Декларативное задание прав модуля

Права своих моделей модуль объявляет атрибутами класса приложения — наследовать ничего не нужно. Строки access_list по атрибутам BASE_USER_ACL / ROLE_ACL создаёт хук security (ACLHook из security/acl_post_init_mixin.py, подключается в security/app.py): ядро даёт точку расширения — класс AppHook и список App.hooks, базовый App.post_init вызывает хуки там, где модуль зовёт super().post_init(); ядро о правах не знает.

backend/base/crm/leads/app.py
from backend.base.crm.security.acl_post_init_mixin import ACLPerms, ACL

class LeadsApp(App):
    # Права для базовой роли base_user
    BASE_USER_ACL = {
        "lead": ACL.FULL,
        "lead_stage": ACLPerms(create=True, read=True, update=True, delete=False),
    }

    # Права для других ролей — словарь role_code → {model → perms}
    ROLE_ACL = {
        "manager": {
            "lead": ACL.NO_DELETE,
        },
        "viewer": {
            "lead": ACL.READ_ONLY,
        },
    }

    async def post_init(self, app: FastAPI):
        # Роли модуля — до super(): права ложатся в super().post_init()
        await init_module_roles(...)
        await super().post_init(app)

Кто объявляет права: модуль — на свои модели для любых ролей или на любые модели для своих ролей. Права на модели ядра (model, app, system_settings) объявляет security. Строки реестров, по которым создаются права, заполнены раньше post_init security: models — ядро (core/models/models.py, Model.seed в Environment.start_post_init), apps — модуль apps (apps/models/apps.py, App.seed в его post_init, sequence 0).

Готовые пресеты

Пресет C R U D Использование
ACL.FULL ✓ ✓ ✓ ✓ Полный доступ
ACL.READ_ONLY — ✓ — — Просмотр
ACL.NO_DELETE ✓ ✓ ✓ — Менеджеры (не удаляют)
ACL.NO_CREATE — ✓ ✓ ✓ Можно править существующее
ACL.NO_ACCESS — — — — Запрет
ACL.CREATE_READ ✓ ✓ — — Только чтение и создание

Если пресета не хватает — ACLPerms(create=..., read=..., ...) с произвольными флагами.

Идемпотентность

_init_acl создаёт ACL только если для пары (role_id, model_id) ещё нет записи. При повторном запуске post_init ничего не дублируется.

Rules — фильтрация на уровне строк

ACL пропустил пользователя в таблицу — теперь Rules решают, какие именно записи он видит. Это делается через domain — JSON-выражение в формате DotORM-фильтра.

domain JSON

Список условий вида [(field, op, value), ...]. Все условия объединяются через AND.

[
  ["user_id", "=", "{{user_id}}"],
  ["active", "=", true]
]

Подстановки:

  • {{user_id}} или {{user.id}} — ID текущего пользователя.

role_id Many2one<Role> nullable

Роль, на которую распространяется правило. NULL = для всех.

perm_create / perm_read / perm_update / perm_delete bool

Для каких операций применяется domain. Например, можно «читать всё, но обновлять только своё».

OR между правилами

Если у пользователя несколько правил на одну модель + операцию, они объединяются через OR: достаточно попасть под любое правило, чтобы запись стала видна.

graph LR
    Q[SELECT * FROM leads]
    Q --> R1["Rule 1: user_id=me"]
    Q --> R2["Rule 2: team_id IN (1,2)"]
    R1 -->|OR| FINAL["WHERE user_id=me<br/>OR team_id IN (1,2)"]
    R2 -->|OR| FINAL

Примеры

[["user_id", "=", "{{user_id}}"]]

Менеджер видит только свои лиды.

[
  ["user_id", "=", "{{user_id}}"],
  ["team_id", "in", [1, 2, 3]]
]

Все условия объединены AND: «свои И из определённых команд». Если хочется «свои ИЛИ из команд» — это два разных Rule на одну модель/операцию (объединятся OR на уровне системы).

[["active", "=", true]]

Архивные записи скрыты от роли.

Поток проверки доступа

sequenceDiagram
    participant U as User
    participant M as Middleware
    participant ORM as DotORM
    participant ACL as AccessList
    participant RU as Rule

    U->>M: Request + Bearer token
    M->>M: set_access_session(session)
    M->>ORM: Model.search(...)
    ORM->>ACL: read=true для роли?
    alt ACL запрещает
        ACL-->>ORM: 403
        ORM-->>U: PermissionDenied
    else ACL разрешает
        ACL-->>ORM: ok
        ORM->>RU: domain для роли + read?
        RU-->>ORM: WHERE (user_id=me) OR (...)
        ORM->>ORM: SQL: SELECT ... WHERE base_filter AND (rule_filter)
        ORM-->>U: Только разрешённые записи
    end

SystemSession — обход проверок

Для серверного кода (cron, post_init, миграции) используется SystemSession:

from backend.base.crm.security.models.sessions import SystemSession
from backend.base.system.dotorm.dotorm.access import set_access_session

# Полный доступ ко всем операциям, без проверок Rule
set_access_session(SystemSession(user_id=SYSTEM_USER_ID))

Только для серверного кода

SystemSession обходит ACL и Rules. Никогда не подставляй её для пользовательских запросов — это эквивалент sudo.

В прикладном коде права поднимают точечно через .sudo() на модели или записи: await env.models.system_settings.sudo().get_value("turn.secret"). Как в Odoo, sudo() поднимает права, но не меняет пользователя: автор записи (create_user_id) и владелец по умолчанию остаются вызывающим, а в публичной ручке — анонимным пользователем (id=4). Системный пользователь подставляется только там, где вызывающего нет: post_init, cron, вебхуки.

Связь с пользователем

# Назначить роль
user.role_ids = [role_admin, role_manager]
await user.save()

# Получить пользователя по роли
managers = await User.search(filter=[("role_ids", "=", manager_role_id)])

См. также