RBAC implementation patterns: API Gateway enforces authz via dedicated AuthZ service backed by Redis cache and auth_db (users/roles/permissions). OPA sidecar overlays ABAC. Admin role mutations write to auth_db, append to audit_log, publish invalidation events through pub/sub to all authz pods which DEL the user's cache key. Three scenarios: cached gateway check (hot path), hierarchical role inheritance with recursive CTE expansion on cache miss, and role revoke with sub-second cache invalidation contrasted against JWT-claims staleness.
08-security/authz-models.md объясняет различия между RBAC, ABAC и ReBAC как моделями. Этот скрипт — про конкретику реализации RBAC в большой системе: где enforce проверку, как моделировать иерархию ролей, как кешировать permissions без stale-окна в часы, и как пропагандировать revoke за миллисекунды, а не за час TTL JWT.
Реальная задача: multi-tenant SaaS, 100+ permissions, 50+ ролей, 10K RPS. Если каждый запрос делает 5 SQL на auth_db — БД расплавится. Если кешировать в JWT с TTL 1 час — admin отозвал админку, а юзер ещё час может удалять документы. Это про trade-off между скоростью, freshness и сложностью.
Цель урока — чтобы инженер мог развернуть AuthZ-слой, который держит нагрузку, не имеет «дыры» на revocation и логирует каждое изменение для compliance.
«RBAC — это map
user → roles → permissions, наложенная наscope(org, project). Сложность не в самой модели, а в четырёх осях: где enforce (gateway vs service), как разворачивать иерархию (recursive CTE vs materialized), как кешировать (Redis vs JWT vs in-process) и как инвалидировать (TTL vs pub/sub vs revocation list).»
Рабочая интуиция: представь, что AuthZ — это read-heavy database. Записей мало (admin раз в день меняет роль), чтений много (на каждый API call). Значит, оптимизируй read path: кеш + denormalized permset. И отдельно — invalidation pipeline, чтобы write были видны быстро.
Пять групп, отражающие реальный production-layout:
user (обычный запрос) и admin (управляет ролями). Разделены, потому что у них разные нагрузочные характеристики: user даёт RPS, admin даёт write на auth_db.gateway (50K RPS, 4 реплики), authz сервис (30K RPS, 3 реплики), и opa sidecar для ABAC overlay. Enforcement на gateway — ADR: проверяй на входе, чтобы downstream сервисы не дублировали логику.redis (100K RPS, TTL 10 минут) и pubsub для broadcasting invalidation events.docs-svc и billing-svc, которые получают уже проверенные запросы.auth_db (users, roles, permissions, user_roles, role_permissions, role_inheritance) и audit_log. Реплик у auth_db всего 2: write-нагрузка низкая, read закрыт кешем.Edges моделируют физические соединения, а не направление данных. pubsub → authz подписочный канал; authdb → audit — write-fanout; gateway → docs-svc — proxy после allow-решения.
Базовый happy path. user шлёт GET /docs/123 с JWT. Gateway верифицирует подпись, извлекает sub=user-42, и вместо того чтобы доверять claims из токена, спрашивает AuthZ сервис «можно ли user-42 делать documents:read на doc-123». AuthZ дёргает Redis (GET perms:user-42:project-100), получает hit за 1ms, отвечает ALLOW. Gateway проксирует на docs-svc, возвращает 200.
Тотал: 3ms на authz-decision. Это и есть price tag за «правильно сделанный RBAC».
ADR: enforce на gateway, а не только в сервисе. Сервис может (и должен) делать defence-in-depth, но primary enforcement на edge — иначе каждый сервис будет тащить authz SDK, и любое изменение модели = деплой всех сервисов.
Сложный случай: DELETE /docs/123, причём у user-42 нет роли в project-100, но есть org-admin в org-1 (project-100 принадлежит org-1). Redis выдаёт MISS — кеш пустой после деплоя или TTL вышел.
AuthZ грузит из auth_db все роли user-42 в scope chain (project-100 → org-1 → global), затем разворачивает иерархию через WITH RECURSIVE: org-admin → admin → editor → viewer, объединяет permission sets и проверяет, есть ли documents:delete. Есть — через inheritance от org-admin. AuthZ заполняет Redis (SET perms:... EX 600) и отвечает ALLOW.
Pitfall, который urok вызывает: materialized expansion (pre-compute всех inherited permissions при role assignment) даёт O(1) lookup, но дрейфует при изменении иерархии. Если ты добавил permission в роль editor, нужно пересчитать всех, у кого admin, owner и т.д. Recursive CTE дороже на чтение, но всегда корректен. Выбор зависит от read/write ratio и tolerability stale данных.
Самый коварный сценарий. Admin делает revoke editor у user-42 в project-100. auth_db пишет DELETE FROM user_roles ..., синхронно льёт запись в audit_log (compliance: SOC 2, GDPR, любой incident требует «кто, когда, что»). Затем publishes событие perms.invalidate user-42 в pubsub.
Все pod-ы AuthZ сервиса подписаны на канал — каждый делает DEL perms:user-42:project-100 в Redis. Propagation < 100ms.
Следующий запрос user-42 (PUT /docs/123, JWT ещё старый) попадает на gateway → authz → Redis MISS → reload из auth_db → permset без editor → 403. Revocation сработал, несмотря на «свежий» JWT.
ADR-pitfall: если хранить permissions только в JWT claims, stale window = TTL JWT. Это значит, что revoked admin может удалять данные ещё час. В production обычно гибрид: JWT с коротким TTL (5 мин) + Redis кеш + pubsub invalidation. Для critical actions (transfer money, delete user) — всегда лезть в DB, не верить кешу.
Дополнительный нюанс: ключ кеша — perms:user-42:project-100, а не просто perms:user-42. Resource-scoped key = blast radius при invalidation один scope, а не все scope юзера. Если у user-42 ролей в 100 org, не нужно ронять кеш всех 100 при revoke в одной.
| Решение | Плюсы | Минусы | Когда выбирать |
|---|---|---|---|
| Enforce на gateway | Единая точка контроля, downstream proof-of-decision | Gateway = SPOF, требует hardening | Всегда как primary. Defence-in-depth в сервисах опционально |
| Recursive CTE для иерархии | Always-correct, easy schema migration | O(depth) на каждый MISS, ~5-20ms | Read/write ratio < 10K, частые изменения иерархии |
| Materialized expansion | O(1) lookup, нет CTE на DB | Drift на hierarchy change, rebuild весь user_perms | Стабильная иерархия, очень read-heavy |
| JWT-cached perms | Stateless, zero hop | Stale window = TTL, revocation сложна | Service-to-service с коротким TTL (1-5 мин) |
| Redis cache + TTL | Простая инвалидация, fast | Hop +1-3ms, Redis dependency | Default для большинства production |
| Redis + pub/sub invalidation | Revocation < 100ms | Pub/sub infrastructure, eventual consistency | Когда revocation критична (compliance, security) |
| In-process LRU | Микросекунды | Sync между подами — pub/sub anyway | Hot read paths, edge enforcement |
| Resource-scoped cache key | Малый blast radius на invalidate | Больше ключей, fragmentation | Multi-tenant с большим scope-cardinality |
Главный trade-off, который определяет архитектуру: сколько ты готов терпеть stale revocation. Час = JWT-only. Минуты = Redis + TTL. Секунды = pub/sub. Real-time = always-DB-query (и ты упёрся в RPS).
if user.role == 'admin' в 100 endpoint. Рефакторинг через год = переписать всё. Centralize в can(user, action, resource, scope).{% if user.is_admin %}<button>Delete</button>{% endif %}. Bypassed trivially через прямой API-call. Backend MUST re-check.auth_db. БД melt. Кешируй.super_admin без logging — abuse не обнаружить. Лучше explicit impersonate flow с обязательным audit.user_id напрямую — N×M permissions, неуправляемо. Всегда через role.if X and Y and not Z. Невозможно audit, легко пропустить ветку.WHERE org_id = ? в SQL. Любая фильтрация по тенанту должна быть обязательной на уровне ORM/middleware.perms:user-42 без scope — invalidation в одной org роняет кеш во всех 100 org юзера. Scope в ключ.RBAC — это «грубый инструмент». Не используй его как единственный механизм, когда:
created_by == user.id». Это ABAC, добавь OPA/Cedar overlay.if user.role == 'admin' достаточен.RBAC хорошо ложится на «у нас есть отделы, у отделов есть права, новые сотрудники наследуют права своего отдела». Если твоя задача про это — бери RBAC. Если про что-то другое — комбинируй.
08-security/authz-models (RBAC vs ABAC vs ReBAC), 08-security/api-security (rate limits, JWT), 08-security/authn-models (как получить sub в JWT).