🔑 RBAC и роли

Back-office - ролевая модель с матрицей прав. Кабинет - изоляция по компании (RBAC-права внутри кабинета не проверяются, роль membership'а формальная). Определения: backoffice/app/rbac.py (роли+права), backoffice/app/auth.py (проверка), cabinet/app/auth.py (изоляция).

Роли back-office

Роль Смысл Права
ceo владелец все (wildcard *, включая управление операторами users.*)
operator операционист клиенты/счета/платежи/FX/лимиты/карты/тарифы + AML raise/screen
compliance комплаенс-офицер ревью AML/Risk/KYC/SAR + скрининг (владеет комплаенсом)
viewer наблюдатель только чтение (*.view)

Присваиваемые в UI роли: ceo, operator, compliance, viewer. Управление операторами (создать/сменить роль/блок/локаут/сброс пароля) - CEO-only, с self-guard и last-CEO-guard.

Матрица прав (полная)

Право ceo operator compliance viewer
clients.view
clients.manage - -
kyc.view
kyc.manage - -
accounts.view -
accounts.manage - -
ledger.view
payments.view
payments.approve - -
fx.view
fx.manage - -
audit.view
reports.view
limits.view
limits.manage - -
messages.view -
messages.send -
aml.view
aml.raise -
aml.review - -
aml.screen -
risk.view
risk.manage - -
sanctions.view
cards.view
cards.manage - -
cards.reveal - -
fees.view
fees.manage - -
revenue.view
accounting.view
accounting.manage - -
sar.view
sar.manage - -

📌 Разделение обязанностей: ревью AML (aml.review), risk-override (risk.manage), управление KYC (kyc.manage) и владение SAR (sar.manage) - у compliance, НЕ у operator. Operator двигает операции, compliance проверяет. CEO может всё.

Механизм проверки прав

Право проверяется зависимостью requires("<permission>") на роуте: она достаёт текущего оператора из сессии и вызывает operator_can(role, permission). Логика: роль с wildcard * (CEO) → всё разрешено; иначе право должно быть в явном наборе роли. Иначе - HTTP 403. Роль хранится в Operator.role.

Сессия и аутентификация оператора

Подписанная cookie-сессия (capital_session, TimestampSigner), sliding-таймаут (обновляется на каждом авторизованном запросе). Пароли - bcrypt. Локаут: 5 неудач → блок на 15 мин (failed_login_attempts/locked_until). Опциональная 2FA (TOTP): opt-in, при включённом TOTP логин двухшаговый (short-lived pending-токен → verify); CEO без 2FA не блокируется.

Изоляция кабинета (client_id / company_id)

Кабинет - хард-изоляция по компании, а не роли. Сессия несёт пару (user_id, company_id). Зависимость get_current_company одним запросом проверяет, что активная company_id есть в membership'ах пользователя (join MembershipCompany); чужой company_id в сессии → 401. Неактивная компания → 403. Это linchpin безопасности кабинета (cabinet/app/auth.py, район строки 220). Все эндпоинты берут компанию через эту зависимость и фильтруют выборки по company.id в SQL. Роль в Membership (default owner) присутствует, но внутри кабинета не проверяется - все члены компании имеют равный доступ к её данным.

CSRF (оба приложения)

Синхронайзер-токен на всех мутирующих запросах (POST/PUT/DELETE) через зависимость verify_csrf: сверяет cookie-токен с токеном из заголовка X-CSRF-Token или form-поля csrf_token (constant-time compare). Безопасные методы (GET/HEAD/OPTIONS/ TRACE) пропускаются. Cookie: capital_csrf (back-office) / capital_cab_csrf (кабинет).

Сводка

Аспект Back-office Кабинет
Идентичность Operator (login+пароль) CabinetUser (email+пароль) + Membership
Сессия ID оператора пара (user_id, company_id)
Роли ceo/operator/compliance/viewer owner (не проверяется)
Модель прав роль → набор прав, requires() нет (равный доступ по membership)
Изоляция однотенантно хард по company_id на каждом запросе
CSRF синхронайзер-токен синхронайзер-токен