6. RBAC, ABAC модели
Модели контроля доступа: RBAC, ABAC и механическое сочувствие к кэшу
«Существуют два метода проектирования архитектуры: первый — сделать ее настолько прозрачной, чтобы дефектов заведомо не было; второй — сделать ее настолько запутанной, чтобы не было видно явных дефектов».
— Сэр Чарльз Энтони Ричард Хоар
После того как личность пользователя надежно установлена (аутентификация), бэкенд встает перед вторым фундаментальным вопросом: что именно этому пользователю разрешено делать? Это зона ответственности авторизации (Authorization / Access Control).
В архитектуре распределенных систем выбор модели контроля доступа определяет не только гибкость бизнес-логики, но и физический профиль производительности сервера: нагрузку на сборщик мусора Go, частоту промахов в аппаратном кэше процессора (CPU Cache Misses) и накладные расходы сетевых вызовов.
Чтобы осязаемо прочувствовать разницу между двумя ключевыми парадигмами — RBAC и ABAC, представим систему допуска на охраняемый ядерный объект:
- RBAC (Role-Based Access Control): Цветной бейдж на шее сотрудника. Уборщик (
viewer), лаборант (editor), главный инженер (admin). Охранник на турникете смотрит только на цвет пластика: «Красный бейдж имеет доступ к пульту управления? Да». Проверка мгновенна, не требует звонков и укладывается в доли наносекунды (прямой аналог попадания в L1-кэш CPU). - ABAC (Attribute-Based Access Control): Многостраничный протокол спецпропуска. Проверяется не просто статус человека, а совокупность динамических условий: «Является ли сотрудник начальником смены И создана ли аварийная заявка менее 30 дней назад И входит ли он в здание в рабочие часы с 08:00 до 20:00 И совпадает ли его геопозиция по GPS с периметром объекта?». Гибкость колоссальная, но каждый шаг требует сбора контекста, опроса внешних датчиков и вычисления сложных логических предикатов.
Выбор модели контроля доступа: скорость против гибкости
flowchart TD
Req["Входящий запрос"] --> Auth["Аутентификация успешна"]
Auth --> ModelDecision{"Выбор модели авторизации"}
ModelDecision -->|"Статические роли и права"| RBAC["RBAC (Role-Based)"]
ModelDecision -->|"Динамические атрибуты и контекст"| ABAC["ABAC (Attribute-Based)"]
RBAC --> R1["Поиск роли пользователя"]
R1 --> R2["Поиск пары: ресурс - действие"]
R2 --> R3["Мгновенный кэш-хит: L1-L2 (наносекунды)"]
ABAC --> A1["Сбор атрибутов пользователя"]
ABAC --> A2["Сбор атрибутов ресурса"]
ABAC --> A3["Сбор контекста окружения (время, IP)"]
A1 --> PDP["Оценка политик: PDP (Rego / Cedar)"]
A2 --> PDP
A3 --> PDP
PDP --> A4["Вычисление булева результата"]
style Auth fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style R3 fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#f8fafc
style PDP fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#f8fafc
RBAC: Матрица прав и кэш-локальность
В модели RBAC пользователю присваивается одна или несколько дискретных ролей (admin, editor, viewer). Каждой роли жестко сопоставлен набор разрешений в формате (ресурс:действие).
Главное инженерное преимущество RBAC — абсолютная детерминированность. Проверка сводится к элементарному поиску в памяти со сложностью $O(1)$.
package authz
import (
"context"
"errors"
"sync"
)
// RBACChecker реализует матрицу прав доступа
type RBACChecker struct {
mu sync.RWMutex
// role -> resource -> action -> allowed
policies map[string]map[string]map[string]bool
}
func NewRBACChecker() *RBACChecker {
return &RBACChecker{
policies: make(map[string]map[string]map[string]bool),
}
}
// AddPolicy поточно-безопасно конфигурирует матрицу прав
func (r *RBACChecker) AddPolicy(role, resource, action string) {
r.mu.Lock()
defer r.mu.Unlock()
if r.policies[role] == nil {
r.policies[role] = make(map[string]map[string]bool)
}
if r.policies[role][resource] == nil {
r.policies[role][resource] = make(map[string]bool)
}
r.policies[role][resource][action] = true
}
// Check проверяет наличие разрешения за минимальное число тактов CPU
func (r *RBACChecker) Check(ctx context.Context, role, resource, action string) error {
r.mu.RLock()
defer r.mu.RUnlock()
// 🔒 Чтение структуры вложенных мап
if resMap, resOk := r.policies[role]; resOk {
if actMap, actOk := resMap[resource]; actOk && actMap[action] {
return nil
}
}
return errors.New("forbidden: insufficient permissions")
}
ABAC: Динамическая оценка и цена вычислений
Когда в системе появляются требования вида:
«Менеджер может редактировать документ, только если документ принадлежит его департаменту, создан менее 30 дней назад, а запрос отправлен с корпоративного IP в рабочие часы», — модель RBAC бессильна. Попытка выразить это через роли приводит к взрывному размножению ролей («ролевой взрыв» / Role Explosion): manager_dep_finance_office_hours и т.д.
Здесь на сцену выходит ABAC (Attribute-Based Access Control). Решение о допуске вычисляется как строгая функция от предикатов:
$$P(\text{user\_attrs}, \text{resource\_attrs}, \text{action}, \text{env}) \to \text{bool}$$
(где P(user_attrs, resource_attrs, action, env) -> bool).
package authz
import (
"context"
"time"
)
// ABACPolicy определяет интерфейс для динамической оценки прав
type ABACPolicy interface {
Evaluate(ctx context.Context, req AccessRequest) (bool, error)
}
// AccessRequest аккумулирует контекст запроса, пользователя и ресурса
type AccessRequest struct {
UserID string
UserRole string
UserDept string
ResourceID string
ResourceAge time.Duration
Action string
ClientIP string
RequestTime time.Time
}
type SimpleABACPolicy struct{}
func (p *SimpleABACPolicy) Evaluate(ctx context.Context, req AccessRequest) (bool, error) {
// 🔒 Многофакторная проверка атрибутов
if req.UserRole != "manager" {
return false, nil
}
if req.UserDept != getOwnerDept(req.ResourceID) {
return false, nil
}
if req.ResourceAge > 30*24*time.Hour {
return false, nil
}
if !isOfficeHours(req.RequestTime) {
return false, nil
}
return true, nil
}
func getOwnerDept(resID string) string { return "engineering" }
func isOfficeHours(t time.Time) bool { return t.Hour() >= 9 && t.Hour() <= 18 }
В распределенном энтерпрайзе правила ABAC выносятся в выделенный компонент — Policy Decision Point (PDP). Индустриальными стандартами стали движки Open Policy Agent (OPA) с декларативным языком Rego, а также библиотека Amazon Cedar. Интеграция с Go осуществляется либо через встраиваемый модуль WASM, либо через gRPC/HTTP вызовы к внешнему сервису.
Механическое сочувствие: ветвления, аллокации и GC
Сравним физические характеристики исполнения двух подходов на процессоре x86-64:
| Инженерный параметр | RBAC | ABAC |
|---|---|---|
| Аллокации на запрос | 0–1 (поиск по ключу в хэш-таблице) | Высокие (сбор map[string]any, динамические структуры атрибутов в куче) |
| Предсказание ветвлений (Branch Prediction) | Высочайшее (предсказуемые проверки role == X) |
Низкое (сложное ветвящееся дерево условий, сбросы конвейера) |
Давление на GC (GC pressure) |
Минимальное (матрица живет в памяти статически) | Значительное (генерация сотен короткоживущих объектов) |
| Синхронизация конкурентности | sync.RWMutex или atomic указатель на матрицу |
Зависит от движка (в OPA — lock-free индекс) |
Динамический сбор атрибутов для ABAC провоцирует лавину аллокаций. В рантайме Go это вызывает частые циклы Minor GC и фазы Stop-The-World, катастрофически деградируя перцентиль латентности P99.
Для спасения производительности в Go применяют пул переиспользуемых объектов sync.Pool:
// ✅ Промышленный паттерн: пул запросов со строгим сбросом памяти
var reqPool = sync.Pool{
New: func() any {
return &AccessRequest{}
},
}
func evaluateWithPool(ctx context.Context, policy ABACPolicy, rawUserID, rawRole, rawDept, resID, ip string) (bool, error) {
// Извлекаем объект из пула
req := reqPool.Get().(*AccessRequest)
// Заполняем поля
req.UserID = rawUserID
req.UserRole = rawRole
req.UserDept = rawDept
req.ResourceID = resID
req.ClientIP = ip
req.RequestTime = time.Now()
// 🔒 Важнейшая деталь: sync.Pool НЕ очищает поля автоматически!
// Сброс состояния должен происходить перед возвратом в пул.
defer func() {
*req = AccessRequest{} // Зануляем структуру
reqPool.Put(req)
}()
return policy.Evaluate(ctx, *req)
}
Гибридные подходы и стандарты индустрии
В высоконагруженных промышленных системах выбор «или-или» не работает. Индустриальным стандартом является каскадная (эшелонированная) модель:
- Быстрый путь (Fast Path — RBAC / ReBAC):
Проверка статических прав и графа отношений (ReBAC — Relationship-Based Access Control, в духе Google Zanzibar:user.owner_id == resource.creator_idилиuser.ID == document.OwnerID) выполняется локально в Go-коде за микросекунды. Если доступ безусловно разрешен или запрещен — проверка мгновенно завершается. - Медленный путь (Slow Path — ABAC):
Если быстрого пути недостаточно, запрос передается локальному движку правил OPA (opa/rego). - Кэширование результатов авторизации:
Повторяющиеся проверки кэшируются на 1–5 секунд с помощью высокоэффективных in-memory кэшей (hashicorp/golang-lruилиdgraph/ristretto), что срезает до 95% нагрузки на PDP.
Старые громоздкие стандарты вроде XACML в современном Go-бэкенде практически вытеснены OPA и протоколами OAuth 2.0 Token Introspection.
Итог
- RBAC обеспечивает феноменальную скорость за счет статической структуры, минимальных аллокаций и высокой кэш-локальности. Это дефолтный выбор для 90% задач бэкенда.
- ABAC предоставляет безграничную гибкость контекстных правил, но требует платы процессорными тактами, аллокациями в куче и нагрузкой на GC.
- Механическое сочувствие к рантайму: Избегайте глубоких вложенных мап
map[string]map[...]во избежание промахов кэша CPU. Используйте пулы объектовsync.Poolи атомарную смену политик черезatomic.StorePointer. - Архитектурный компромисс: Стройте каскадную модель — отсекайте большинство запросов локальным RBAC/ReBAC, делегируя встраиваемому ABAC-движку только сложные бизнес-проверки.
Как хранить сессионные данные пользователя на сервере, когда stateless-токенов недостаточно?
В следующей лекции мы разберем классические и современные подходы к управлению сессиями: 7. Session based auth.