Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

RBAC/ABAC без боли: управляем доступами, ускоряем продажи Enterprise и снижаем риски

Разработка и технологии12 апреля 2026 г.
Правильная модель прав доступа — это не только про безопасность. Это меньше инцидентов, быстрее согласования у клиентов и короче сделки в Enterprise. Разберём, как спроектировать RBAC/ABAC с мультиарендой, кешированием, аудитом и фильтрацией данных, чтобы не тормозить разработку и поддержку.
RBAC/ABAC без боли: управляем доступами, ускоряем продажи Enterprise и снижаем риски

  • Зачем бизнесу сильная модель доступа
  • Когда хватает RBAC, а когда нужен ABAC
  • Базовая схема БД для ролей и прав (мультиаренда)
  • Проверка доступа в коде: быстрый путь и кеш
  • Политики на атрибутах: пример с OPA (Rego)
  • Фильтрация списков и RLS в Postgres
  • Интеграция с SSO: маппинг групп в роли
  • Аудит и объяснимость решений
  • Миграция с «флагов в коде» к нормальной модели
  • Производительность и тестирование
  • Анти‑паттерны и частые ошибки
  • Чек‑лист внедрения

Зачем бизнесу сильная модель доступа

Право доступа — это не только про «закрыть от лишних глаз». От него напрямую зависят:

  • скорость сделки в Enterprise: заказчики спрашивают про SSO, роли, аудит, разделение по отделам и странам;
  • количество инцидентов и простои: неверный доступ — утечки, штрафы и потери доверия;
  • издержки поддержки: меньше ручных манипуляций с базой и «пожалуйста, дайте временный доступ»;
  • скорость разработки: понятная модель снижает число «if‑ов», багов и противоречий.

Хорошая модель делает доступы предсказуемыми, проверяемыми и объяснимыми. А ещё — масштабируемыми: не ломаются при росте команд и клиентов.

Когда хватает RBAC, а когда нужен ABAC

  • RBAC (роль‑бэйзд): у пользователя есть роли, роли дают права. Подходит, когда набор действий и гранулярность невелики: «просмотр/редактирование счетов», «админ». Плюсы — простота, быстрый онбординг клиентов. Минусы — гибкости мало: трудно выразить «регион=EMEA», «класс данных=внутренний», «доступ только к своим документам».
  • ABAC (атрибут‑бэйзд): решение строится на атрибутах субъекта, ресурса и окружения: департамент, регион, чувствительность данных, рабочее время, MFA‑статус. Плюсы — гибко. Минусы — сложнее проектировать и проверять, нужна объяснимость.

Практика: начинайте с чистого RBAC, оставьте «крючки» для атрибутов (tenant_id, owner_id, region). Когда бизнес попросит «руководителю — все за свой отдел», добавьте слой ABAC для отдельных ресурсов, не переписывая всё ядро.

Базовая схема БД для ролей и прав (мультиаренда)

Пример на Postgres: роли и права в разрезе арендатора (tenant). Роли и назначения — внутри арендатора, чтобы клиенты не видели друг друга.

-- Арендаторы (компании)
CREATE TABLE tenants (
  id            uuid PRIMARY KEY,
  name          text NOT NULL,
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- Пользователи: могут состоять в нескольких арендаторах
CREATE TABLE users (
  id            uuid PRIMARY KEY,
  email         citext UNIQUE NOT NULL,
  full_name     text,
  created_at    timestamptz NOT NULL DEFAULT now(),
  is_active     boolean NOT NULL DEFAULT true
);

-- Связка пользователь-арендатор (учёт членства и атрибутов)
CREATE TABLE user_tenants (
  user_id       uuid REFERENCES users(id) ON DELETE CASCADE,
  tenant_id     uuid REFERENCES tenants(id) ON DELETE CASCADE,
  department    text,  -- атрибут для ABAC
  region        text,  -- атрибут для ABAC
  PRIMARY KEY (user_id, tenant_id)
);

-- Права как действия над типом ресурса
CREATE TABLE permissions (
  id            serial PRIMARY KEY,
  code          text UNIQUE NOT NULL,  -- например, invoice.read, invoice.write
  description   text
);

-- Роли внутри арендатора
CREATE TABLE roles (
  id            uuid PRIMARY KEY,
  tenant_id     uuid REFERENCES tenants(id) ON DELETE CASCADE,
  name          text NOT NULL,
  is_system     boolean NOT NULL DEFAULT false, -- системные роли, нельзя удалять
  UNIQUE (tenant_id, name)
);

-- Разрешения, включённые в роль
CREATE TABLE role_permissions (
  role_id       uuid REFERENCES roles(id) ON DELETE CASCADE,
  permission_id int REFERENCES permissions(id) ON DELETE RESTRICT,
  PRIMARY KEY (role_id, permission_id)
);

-- Роли, назначенные пользователю в рамках арендатора
CREATE TABLE user_roles (
  user_id       uuid,
  tenant_id     uuid,
  role_id       uuid REFERENCES roles(id) ON DELETE CASCADE,
  expires_at    timestamptz,
  PRIMARY KEY (user_id, tenant_id, role_id),
  FOREIGN KEY (user_id, tenant_id) REFERENCES user_tenants(user_id, tenant_id) ON DELETE CASCADE,
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

-- Пример бизнес-ресурса: счета
CREATE TABLE invoices (
  id            uuid PRIMARY KEY,
  tenant_id     uuid REFERENCES tenants(id) ON DELETE CASCADE,
  owner_id      uuid REFERENCES users(id),  -- для ABAC: свои/чужие
  region        text,                        -- для ABAC
  amount_cents  int NOT NULL,
  status        text NOT NULL CHECK (status IN ('draft','sent','paid','canceled')),
  created_at    timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_user_tenants_by_user ON user_tenants (user_id);
CREATE INDEX idx_user_roles_by_user_tenant ON user_roles (user_id, tenant_id);
CREATE INDEX idx_invoices_tenant ON invoices (tenant_id);

Что важно:

  • все ключи содержат tenant_id — это упрощает фильтрацию и изоляцию;
  • в user_roles есть expires_at для временных доступов;
  • ресурс хранит атрибуты (owner_id, region), пригодные для ABAC и RLS.

Проверка доступа в коде: быстрый путь и кеш

Минимальный, но быстрый путь: вытянуть разрешения пользователя в рамках арендатора, кешировать на короткий срок (например, 60 секунд) с инвалидацией через Pub/Sub. Не кладите список разрешений навечно в JWT — лишитесь мгновенного отзыва.

Пример на Python (FastAPI) с локальным LRU и Redis для инвалидации:

# requirements: fastapi, uvicorn, redis, asyncpg, cachetools
import asyncio
import json
from typing import Set

import asyncpg
from cachetools import TTLCache
from fastapi import FastAPI, Request, HTTPException, Depends
import redis.asyncio as redis

app = FastAPI()

POOL: asyncpg.Pool
R = redis.Redis(host="localhost", port=6379, decode_responses=True)
PERM_CACHE = TTLCache(maxsize=10000, ttl=60)  # 60 секунд

async def init_pool():
    global POOL
    POOL = await asyncpg.create_pool(dsn="postgresql://user:pass@localhost/db")

@app.on_event("startup")
async def startup():
    await init_pool()
    # Подписываемся на канал инвалидации
    async def listener():
        pubsub = R.pubsub()
        await pubsub.subscribe("perm:invalidate")
        async for msg in pubsub.listen():
            if msg.get("type") == "message":
                key = msg.get("data")
                PERM_CACHE.pop(key, None)
    asyncio.create_task(listener())

async def fetch_permissions(user_id: str, tenant_id: str) -> Set[str]:
    cache_key = f"{user_id}:{tenant_id}"
    if cache_key in PERM_CACHE:
        return PERM_CACHE[cache_key]
    async with POOL.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT p.code
            FROM user_roles ur
            JOIN role_permissions rp ON rp.role_id = ur.role_id
            JOIN permissions p ON p.id = rp.permission_id
            WHERE ur.user_id = $1 AND ur.tenant_id = $2
              AND (ur.expires_at IS NULL OR ur.expires_at > now())
            """,
            user_id, tenant_id,
        )
    perms = {r["code"] for r in rows}
    PERM_CACHE[cache_key] = perms
    return perms

async def require_permission(code: str):
    async def dep(request: Request):
        # Получите user_id и tenant_id из токена/сессии
        user_id = request.headers.get("X-User-Id")
        tenant_id = request.headers.get("X-Tenant-Id")
        if not user_id or not tenant_id:
            raise HTTPException(status_code=401, detail="unauthorized")
        perms = await fetch_permissions(user_id, tenant_id)
        if code not in perms:
            raise HTTPException(status_code=403, detail="forbidden")
        return {"user_id": user_id, "tenant_id": tenant_id}
    return dep

@app.post("/invoices/{invoice_id}:send")
async def send_invoice(invoice_id: str, ctx=Depends(require_permission("invoice.write"))):
    # Дальше — бизнес-логика
    return {"status": "ok"}

# Инвалидация при изменении ролей: R.publish("perm:invalidate", f"{user_id}:{tenant_id}")

Где брать user_id/tenant_id:

  • из краткоживущего JWT (5–10 минут) с идентификаторами и хешем ролей;
  • из серверной сессии, если у вас собственный шлюз.

Политики на атрибутах: пример с OPA (Rego)

Когда RBAC уже не хватает, добавьте слой политик. Один из вариантов — отдельный движок политик (например, OPA). Он получает входные атрибуты и отвечает «permit/deny» плюс обоснование.

Пример политики на Rego: «Пользователь может читать счёт, если он админ, или владелец, или из того же региона; редактировать — только владелец или роль бухгалтер».

package access.invoice

default allow = false

# Входные данные (пример):
# input = {
#   "perms": ["invoice.read", "invoice.write"],
#   "user": {"id": "u1", "department": "sales", "region": "EMEA"},
#   "tenant_id": "t1",
#   "resource": {"owner_id": "u2", "region": "EMEA", "status": "draft"},
#   "action": "read"
# }

is_admin {
  some p
  p := input.perms[_]
  p == "admin"
}

allow {
  input.action == "read"
  ("invoice.read" == input.perms[_])
}

allow {
  input.action == "read"
  is_admin
}

allow {
  input.action == "read"
  input.user.region == input.resource.region
}

allow {
  input.action == "write"
  ("invoice.write" == input.perms[_])
  input.user.id == input.resource.owner_id
}

allow {
  input.action == "write"
  ("accountant" == input.perms[_])
}

Как вызывать:

  • формируете вход: разрешения пользователя (из кеша), атрибуты пользователя (департамент, регион), сведения о ресурсе (владелец, регион, статус), действие;
  • получаете allow=true/false и объяснение через opa eval с флагом explain (для аудита).

Важно: политики — код. Держите их в репозитории, с обзорами, версиями и тестами.

Фильтрация списков и RLS в Postgres

Проверка одного объекта — половина дела. Вторая половина — выдавать правильные списки. Иначе пользователь увидит лишние айдишники или «дыры» в пагинации.

Подходы:

  • Фильтровать в коде: WHERE tenant_id = $1 AND (region = $2 OR owner_id = $3). Просто, прозрачно, но легко ошибиться и забыть условие.
  • Политики на уровне строк (RLS) в Postgres: база сама не отдаст лишнее. Удобно для строгой изоляции.

Пример RLS для таблицы invoices:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

-- Роль приложения (используйте SET ROLE в пуле)
CREATE ROLE app_user NOLOGIN;
GRANT SELECT, INSERT, UPDATE, DELETE ON invoices TO app_user;

-- Функция возвращает контекст запроса (tenant_id, user_id, region)
CREATE OR REPLACE FUNCTION current_ctx() RETURNS jsonb LANGUAGE sql STABLE AS $$
  SELECT current_setting('app.ctx', true)::jsonb;
$$;

-- Политика чтения: только строки своего арендатора и либо свой регион, либо свой объект
CREATE POLICY inv_read ON invoices FOR SELECT TO app_user USING (
  (current_ctx()->>'tenant_id')::uuid = tenant_id
  AND (
    region = current_ctx()->>'region'
    OR owner_id = (current_ctx()->>'user_id')::uuid
  )
);

-- Политика изменения: только владелец в статусе draft
CREATE POLICY inv_update ON invoices FOR UPDATE TO app_user USING (
  (current_ctx()->>'tenant_id')::uuid = tenant_id
  AND owner_id = (current_ctx()->>'user_id')::uuid
  AND status = 'draft'
);

В коде при взятии соединения перед каждым запросом устанавливайте контекст:

SELECT set_config('app.ctx', '{"tenant_id":"t1","user_id":"u1","region":"EMEA"}', false);

Плюсы RLS: «не забыть фильтр». Минусы: сложнее отлаживать, нужны строгие практики миграций и тестов. Комбинируйте: простые условия — RLS, сложные сценарии — слой политик (OPA) перед обращением к БД.

Интеграция с SSO: маппинг групп в роли

Enterprise‑клиенты ждут: вход через корпоративный SSO (SAML/OIDC), группы и отделы приходят в токене. Ваша задача — аккуратно сопоставить это ролям внутри арендатора.

Практика:

  • храните таблицу соответствий: external_group -> role_id для tenant_id;
  • не доверяйте группам без белого списка: маппинг только для заранее настроенных значений;
  • поддержите SCIM‑провижининг (по возможности) — уменьшите ручную работу админов.

Схема соответствий:

CREATE TABLE sso_group_mapping (
  tenant_id   uuid REFERENCES tenants(id) ON DELETE CASCADE,
  provider    text NOT NULL, -- 'saml' или 'oidc'
  ext_group   text NOT NULL, -- значение из токена SSO
  role_id     uuid REFERENCES roles(id) ON DELETE CASCADE,
  PRIMARY KEY (tenant_id, provider, ext_group)
);

Алгоритм входа:

  1. получаем утверждения SSO (groups, department, email);
  2. находим соответствующие роли для tenant_id;
  3. выдаём пользователю временные назначения user_roles (или динамически подмешиваем разрешения в сессию).

Аудит и объяснимость решений

Без этого вы упрётесь в проверки служб безопасности клиента.

  • Логируйте: кто, к какому ресурсу, с какими атрибутами, какую политику проходил, решение allow/deny, источник (RBAC/ABAC/RLS);
  • Держите объяснение: «доступ разрешён, потому что роль invoice.read и регион совпадает»;
  • Экспортируйте в SIEM клиента: syslog, HTTPS, S3. Поддержите поиск по пользователю и ресурсу за 1–2 года.

Минимальный формат события аудита:

{
  "ts": "2026-04-12T10:22:31Z",
  "tenant_id": "t1",
  "user_id": "u1",
  "action": "invoice.read",
  "resource": {"type": "invoice", "id": "inv-123", "region": "EMEA"},
  "decision": "allow",
  "reason": ["rbac:invoice.read", "abac:region_match"],
  "request_id": "r-9c2e",
  "ip": "203.0.113.5"
}

Миграция с «флагов в коде» к нормальной модели

  • Этап 1. Инвентаризация действий. Составьте справочник permissions: что реально делает система (read, write, delete, approve). Не дробите чрезмерно.
  • Этап 2. Введите роли‑шаблоны по умолчанию для нового арендатора (admin, accountant, viewer). Дайте клиенту редактировать копии, но защитите системные роли.
  • Этап 3. Перенесите проверки: замените if is_admin… на require_permission("…") в шлюзе/контроллере.
  • Этап 4. Включите кеш и аудит. Сначала — только RBAC.
  • Этап 5. Точечно добавьте ABAC там, где болит: «свои объекты», «по региону», «чувствительные статусы».
  • Этап 6. Для критичных таблиц включите RLS и автотесты на политики.

Производительность и тестирование

  • Горячий кеш разрешений: 95% запросов без похода в БД. Инвалидация через Pub/Sub.
  • Пакетная проверка: для списков запрашивайте только один раз пермиссии и атрибуты, не делайте N вызовов.
  • Трассировка: добавьте в метрики долю 403, время ответа движка политик, число промахов кеша.
  • Нагрузочные тесты: профили с 1k/10k/100k пользователей на арендатора, 100k ролей и 1M назначений. Проверьте P95 и инвалидацию под нагрузкой.
  • Юнит‑тесты на политики: «матрица» из действий × ролей × атрибутов. Для RLS — миграции + explain analyze, чтобы не потерять индексы.

Анти‑паттерны и частые ошибки

  • Список прав в JWT на сутки. Затем клиенту нельзя быстро отозвать доступ. Делайте короткий срок жизни токена или храните только идентификаторы с быстрой проверкой на сервере.
  • Проверки только в интерфейсе. Любой может обойти фронт через API. Политики — на бэкенде и/или на уровне БД (RLS).
  • Смешивание арендаторов в одной роли. Роли и назначения всегда внутри tenant_id.
  • «Суперадмин всё может без логов». Даже суперадмин попадает в аудит и подтверждает чувствительные операции с MFA.
  • Чрезмерная детализация прав: invoice.read.self, invoice.read.department, invoice.read.region… Взлетит сложность. Держите базовые действия в permissions, остальное решайте атрибутами в ABAC.
  • Политики без версионирования. Любая правка — как деплой кода: PR, ревью, тесты, чёткие сообщения об изменениях.

Чек‑лист внедрения

  • Справочник действий (permissions) и системные роли‑шаблоны
  • Модель БД с tenant_id везде, где надо
  • Слой require_permission и кеш с инвалидацией
  • Аудит запросов доступа и экспорт событий
  • Точечные ABAC‑политики и их тесты
  • Фильтрация списков: SQL‑условия и/или RLS
  • Интеграция с SSO и маппинг групп → роли
  • Временные доступы (expires_at) и подтверждения опасных операций (MFA/«четыре глаза»)
  • Нагрузочное тестирование и наблюдаемость (метрики, трассировка)
  • Документация для админов клиента (как настраивать роли и группы)

Итог: начинайте с чёткого RBAC, добавляйте ABAC там, где это даёт бизнес‑ценность (правильные списки, изоляция по регионам, чувствительные статусы). Подкрепляйте решения кешем, аудитом и тестами. Так вы закроете вопросы безопасности, ускорите согласование у крупных клиентов и снимете головную боль у поддержки и разработки.


аудитRBACABAC