Привет! Это Сергей, и я снова тут с 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
Репозиторий: https://github.com/Cheater121/rbacx/
Документация: https://cheater121.github.io/rbacx/
Первая статья: https://habr.com/ru/articles/950080/
Буду рад обратной связи в issues или в комментариях. И звёздочка на GitHub — как всегда, лучший способ сказать «зашло» 🙂
