Привет! Это Сергей, и я снова тут с RBACX. Полгода назад написал первую статью о своей библиотеке — тогда это был скромный движок для RBAC/ABAC авторизации на Python, который я собрал из собственных наработок и решил оформить по-человечески. Статья нашла своих читателей, и это стало хорошим мотиватором не останавливаться.

За это время вышло больше 25 релизов — от мелких фиксов до довольно серьёзных архитектурных изменений. Хочу рассказать, что появилось самого интересного, и почему RBACX сейчас — это совсем не та библиотека, про которую я писал в сентябре.

Если не читали первую статью — она здесь. Там есть предыстория, архитектура и быстрый старт.


Сначала — безопасность (security-фиксы)

Прежде чем рассказывать о фичах, надо честно признать: за этот период выявил три неприятных вещи (которые уже закрыл патчами).

Обход deny-overrides через компилятор. В скомпилированном (быстром) пути оценки политик правила группировались по специфичности ресурса. Оказалось, что если для ресурса совпадал более специфичный набор правил с permit, более широкий набор с deny просто не проверялся. Итог: deny-overrides давал permit там, где не должен был. Это был реальный баг безопасности (надеюсь никого не подставил) — исправил плюс написал 18 регрессионных тестов + 500 рандомизированных проверок эквивалентности двух путей.

DoS (которая атака) через глубокую вложенность условий. В самом сердце библиотеки в eval_condition политик не было ограничения глубины рекурсии для and/or/not. Злобная политика из HTTP/S3-источника могла уронить воркер RecursionError-ом. Теперь есть MAX_CONDITION_DEPTH = 50, ConditionDepthError и fail-closed поведение.

HTTP-источник политик без TLS-верификации и SSRF. HTTPPolicySource тихо игнорировал SSL-ошибки и не валидировал URL (уопс). Добавил verify_ssl, allowed_schemes, block_private_ips — на метадата-эндпоинты AWS типа 169.254.169.254 больше не залетим.

Мораль: если ваш источник политик — внешний URL, обновляйтесь.


ReBAC: «кто кому что разрешил» — и почему я на несколько месяцев забросил либу

Самая большая фича цикла — поддержка relationship-based авторизации (ReBAC).

RBAC говорит «у тебя роль editor». ABAC добавляет «и документ в нужном отделе». ReBAC идёт дальше: «ты являешься владельцем (owner) документа 42, а owner может приглашать других». Это то, как работает Google Drive, Notion, GitHub — когда права определяются графом отношений между сущностями, а не набором атрибутов.

Про ReBAC мне написали в питонском чате. Мол, у тебя неплохая библиотека, но без ребака — неполная. Я сел делать — и это оказался самый трудоёмкий кусок за всё время работы над проектом (даже не знаю, сколько раз я пожалел, что в это ввязался). Местами казалось, что я зарылся в коде и уже не понимаю, правильно ли вообще иду. В итоге всё таки сделал, выпустил — и на несколько месяцев отложил этот проект в сторону, чтобы выдохнуть.

Только спустя немало времени и закрытие других фич я смог вернуться в ребак: SpiceDB-батч метод перевез на один gRPC-вызов вместо N последовательных-одинарных — значительно быстрее при проверке прав на целый список объектов. По итогу в ReBAC’е есть: встроенный локальный граф отношений для тестов и небольших проектов, плюс готовые интеграции с OpenFGA и SpiceDB — оба синхронные и асинхронные.


Пакетная проверка прав: вопрос из комментариев стал фичей

Эту штуку в какой-то степени помогло придумать сообщество.

На Хабре под первой статьёй спросили: подойдёт ли RBACX для PyQt-приложения? Специфичная история (для меня по крайней мере). При чем в UI — сотни кнопок, вкладок и действий, которые надо показывать или скрывать в зависимости от прав пользователя. Проверять каждый элемент отдельным вызовом — накладно.

Вопрос я гонял в голове довольно долго: это ведь проблема не только PyQt. В любом интерфейсе нужно знать «что этому пользователю можно», и делать N отдельных запросов ради этого неловко.

Сделал в главном классе библиотеки (Guard) два метода evaluate_batch_async и evaluate_batch_sync:

decisions = await guard.evaluate_batch_async([
    (subject, Action("read"),   doc, ctx),
    (subject, Action("write"),  doc, ctx),
    (subject, Action("delete"), doc, ctx),
])
# -> List[Decision], в том же порядке

Асинхронный батч работает через asyncio.gather — все проверки идут параллельно, общее время определяет самая медленная, а не сумма.


ИИ-генерация политик: это не шутка

Опять сообщество (питонский чат), опять фидбек (но в этот раз был негативный). Мне сказали мол: твоя либа никому не нужна, джейсоны никто писать не будет для политик (и это похоже на правду, либу действительно наверное мало кто использует). Зачем оно надо, когда есть if user.role == "admin". Но это тоже натолкнуло меня на мысль… Добавил необычную штуку — экстру rbacx[ai] с классом AIPolicy.

Идея простая: описывать политики авторизации руками — скучно и требует глубокого знания DSL. А что если скормить LLM OpenAPI-схему приложения и получить готовую политику? Причем схема например в FastAPI генерируется автоматически.

from rbacx.ai import AIPolicy

ai = AIPolicy(api_key="sk-...", model="gpt-4o")

# Генерируем политику из OpenAPI-схемы
result = await ai.from_schema(openapi_schema)
guard = Guard(result.policy)

Пайплайн безопасный: генерация → валидация JSON Schema → повтор при ошибке → линтер → (опционально) компиляция. Если LLM вернул мусор — ретраи, а не краш (но все таки ограничен, чтобы не разорить владельца ключа, если используется платная модель).

Есть ещё refine_policy для итеративного уточнения на естественном языке и explain_decision — когда LLM объясняет пользователю решение движка человеческим языком. При этом само решение принимает детерминированный движок, а не LLM — безопасность не страдает.

Поддерживаются OpenAI, OpenRouter, Ollama, Azure, Qwen и любые OpenAI-совместимые провайдеры.


Шортхэнд для ролей: простой RBAC стал красивее

Это небольшая, но приятная вещь — синтаксический сахар для самого частого случая (пресловутый if user.role == "admin").

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

{
  "id": "doc_read",
  "effect": "permit",
  "actions": ["read"],
  "condition": {
    "hasAny": [{"attr": "subject.roles"}, ["admin", "editor"]]
  }
}

Теперь можно просто:

{
  "id": "doc_read",
  "effect": "permit",
  "actions": ["read"],
  "roles": ["admin", "editor"]
}

Если нужны и роли, и дополнительное условие — оба поля комбинируются через AND. Если вдруг написать и roles, и condition с проверкой subject.roles одновременно — линтер выдаст предупреждение ROLES_CONDITION_OVERLAP. Не ошибка, просто подсказка что что-то, возможно, лишнее.

Казалось бы мелочь, но когда политика состоит из двадцати правил — она реально становится читабельнее.


Помимо этого

Добавил кэширование, в том числе кэш-адаптер для Redis (rbacx[cache-redis]) — для многопроцессорных и multi-host деплоев, где in-memory кэш не расшаривается между воркерами. Сделал нормальный async-адаптер для Django 4.1+ ASGI — синхронный был с первой версии, давно надо было сделать. Плюс strict types mode — Guard(policy, strict_types=True) отключает неявные приведения типов при матчинге, для тех кто хочет предсказуемость без магии. Также запилил полноценный трейс решений через explain=True — видно, какие правила сработали, какие нет и почему.


О том, как это делается

Раньше все разрабы смотрели на вайбкодеров сверху вниз (я и сам подтрунивал бывало). Сейчас мне кажется уже ошибкой будет не использовать нейронки в своей работе. И честно говоря, я в шоке от возможностей нейросетей. Не в плане «вот молодцы», а в буквальном — я не ожидал, что темп будет таким. Кода стало много и появляется он быстро. Настолько быстро, что сейчас торможу именно я — чтобы осмыслить в голове, что вообще происходит в кодовой базе.

Да, код приходится перепроверять руками. Особенно security-критичные куски — там не бывает «кажется, работает». Но интереснее другое: поймал себя на новом для себя паттерне работы. Сегодня разбираюсь с ядром — спорю с нейросетями, правильно ли там устроена логика, спрашиваю разные модели, сравниваю ответы, гуглю, читаю международные рекомендации, прихожу к своему выводу. Завтра переключаюсь на метрики или кэш — совершенно другой контекст. И так постоянно. Это какая-то новая мультизадачность, к которой я привыкаю. Непривычно, но по-своему интересно.


Что дальше

В планах OpenTelemetry-трейсы вокруг ключевых стадий принятия движком решений (метрики уже есть давно), WebSocket-адаптеры для Starlette/FastAPI, и исполняемые обязательства — когда вместо того чтобы самому разбирать Decision.obligations в приложении, можно будет при инициализации Guard зарегистрировать колбэки: guard.register_obligation_handler("require_mfa", my_mfa_checker) — и движок сам их дёрнет в нужный момент.

И ещё один пункт, по которому интересно мнение сообщества: gRPC PDP. Идея — выставить Guard как gRPC-сервис, чтобы non-Python сервисы (Go, Rust, что угодно) могли обращаться к тому же движку политик. Концептуально звучит красиво. Но Python — не самый быстрый язык, и когда речь идёт об авторизации на горячем пути в полиглот-архитектуре, возникает вопрос «а стоит ли так заморачиваться». Что думаете?


Попробовать

pip install rbacx

Буду рад обратной связи в issues или в комментариях. И звёздочка на GitHub — как всегда, лучший способ сказать «зашло» 🙂