Роли и правила¶
Система прав FARA устроена в три слоя:
- Роли (
Role) — что-то вроде «должности»: набор разрешений, который можно выдать пользователю. - ACL (
AccessList) — права на уровне таблицы: может ли роль читать/создавать/изменять/удалять записи модели в принципе. - 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.
ACLPostInitMixin — декларативное задание прав¶
В post_init модуля права обычно описываются через миксин:
from backend.base.crm.security.acl_post_init_mixin import (
ACLPostInitMixin,
ACLPerms,
ACL,
)
class LeadsApp(ACLPostInitMixin, 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):
await super().post_init(app)
await self._init_acl(app.state.env)
Готовые пресеты¶
| Пресет | 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}}— 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
Примеры¶
Все условия объединены AND: «свои И из определённых команд». Если хочется «свои ИЛИ из команд» — это два разных Rule на одну модель/операцию (объединятся OR на уровне системы).
Поток проверки доступа¶
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.
Связь с пользователем¶
# Назначить роль
user.role_ids = [role_admin, role_manager]
await user.save()
# Получить пользователя по роли
managers = await User.search(filter=[("role_ids", "=", manager_role_id)])
См. также¶
- Иерархия ролей — как наследование через
based_role_idsвлияет на права в реальных сценариях. - Security Module — аутентификация, сессии, ContextVar.