Операционист «Ковчега» спрашивает корпоративного ассистента: «Какие условия по последней сделке с этим клиентом?» Ассистент честно ищет в RAG-индексе, находит документ из юридического отдела и отдаёт. Проблема: у операциониста нет доступа к этому документу в SharePoint. Он не взламывал систему — он просто спросил бота, а бот не субъект прав. Ретривер нашёл самое релевантное, модель пересказала. Так через ИИ происходит привилегированная утечка: данные, к которым у человека нет доступа в источнике, вытекают через ассистента.
Это фундаментальный дефект наивного RAG. Индексация схлопывает данные разных уровней доступа в один векторный индекс, а поиск по смыслу не знает про права. Ниже — как это чинится: от архитектурного паттерна до конкретного стека, шагов реализации и режимов отказа.
Бизнес-цель
Пользователь «Ковчега» получает из RAG ровно то, что ему разрешено видеть в исходных системах — не больше. Обещания бизнесу:
- Ни один ответ не содержит данных, к которым у спросившего нет доступа в источнике.
- Отзыв доступа в источнике отражается в RAG за минуты, не за сутки.
- На вопрос регулятора или аудита «почему этот сотрудник это увидел» ответ лежит в правах, а не в удаче ретривера.
Что именно угрожает системе
- Confused deputy. ИИ действует от своего имени с полными правами индекса, а не от имени пользователя. Классический вектор внутренней утечки.
- OWASP LLM. LLM02 (Sensitive Info Disclosure), LLM06 (Excessive Agency — когда RAG-tools ходят в системы).
- GDPR / банковская тайна. Доступ к персональным данным без основания — нарушение и внутри компании.
- Аудит AI Act. Доступ к данным high-risk должен логироваться и обосновываться.
Архитектурный паттерн: Identity-Aware Hybrid Filtering RAG
Коненсус enterprise-архитектуры 2026 года: не выбирать между быстрым pre-filter и точным authz, а совмещать оба.
JWT(user: groups, roles, tenant_id)
│
▼
[1] Pre-filter в векторной БД по ACL-метаданным (recall, дёшево)
│ кандидаты-чанки
▼
[2] FGA post-check: CheckBulkPermissions по source-документам каждого чанка
│ разрешённые чанки (точность, ReBAC/ABAC)
▼
[3] LLM → [4] OPA post-filter ответа (маскирование полей, запрет комбинаций)
Pre-filter отсекает основную массу дёшево. FGA-сервис даёт точную проверку там, где payload-фильтра мало: сложные отношения, ABAC-контекст. Право пользователя проникает в сам ретрив, а не накладывается на выдачу постфактум.
Инженерный стек
| Слой | Технологии |
|---|---|
| Vector DB с payload-фильтрами | Qdrant, Pinecone, PostgreSQL (pgvector) |
| Identity | OpenID Connect (OIDC), Keycloak; JWT с groups, roles, tenant_id |
| Fine-Grained Authorization (ReBAC/ABAC) | OpenFGA, SpiceDB, Cerbos, Auth0 FGA — CheckBulkPermissions в пути ретрива |
| Policy на выходе | OPA (Rego) |
| Оркестрация | LlamaIndex / LangChain |
Инженерная реализация
Шаг 1. ACL как метаданные чанка + версия
При индексации в payload каждого чанка кладём нормализованный массив ACL и acl_version. Версия — ключ к мгновенной инвалидации.
payload = {
"tenant_id": doc.tenant_id,
"allowed_groups": doc.acl_groups, # из первоисточника
"source_doc_id": doc.id, # для FGA post-check
"acl_version": doc.acl_version,
}
Шаг 2. Pre-filter по контексту безопасности из JWT
flt = {"must": [
{"key": "tenant_id", "match": {"value": user.tenant_id}},
{"key": "allowed_groups","match": {"any": user.groups}},
]}
candidates = qdrant.search(query_vec, query_filter=flt, limit=50)
Шаг 3. FGA post-check по источнику
allowed = fga.batch_check(
user=f"user:{user.id}",
relation="viewer",
objects=[f"doc:{c.payload['source_doc_id']}" for c in candidates],
)
chunks = [c for c in candidates if allowed[c.payload["source_doc_id"]]]
Шаг 4. Синхронизация ACL — событийная, не батчевая
Подписка на permission-change webhooks из источников; изменение прав реконсилится в минуты. Бампаем acl_version → устаревшие чанки инвалидируются. Батчевая ночная синхронизация здесь недопустима: сутки с отозванным доступом — это сутки утечки.
Шаг 5. OPA на выходе
Финальный ответ и цитаты проходят политику: маскирование отдельных полей, запрет опасных комбинаций, редактирование метаданных-ссылок.
Где ломается
- Рассинхрон ACL — главный риск. Права в источнике сменились, индекс отстал → отдаём уже запрещённое. Лечится только событийной синхронизацией и FGA-проверкой в реальном времени. Pre-filter по индексу всегда отстаёт на величину лага.
- Утечка через агрегацию. Разрешённые по отдельности чанки в сумме раскрывают запрещённое. Ни pre-filter, ни FGA этого не видят — нужна логика уровня OPA и бизнес-правил.
- Метаданные-цитаты. Контент отфильтрован, но заголовок или ссылка источника в ответе раскрывает сам факт существования документа. Чистить цитаты тоже.
- Латентность FGA.
CheckBulkPermissionsна десятки чанков добавляет задержку. Балансируйте глубину pre-filter и объём post-check. - ABAC-контекст. «Доступ только в рабочее время / только по своему региону / только с бизнес-обоснованием» плохо ложится в payload — уходит в FGA/OPA, растёт сложность.
Стандарты и маппинг
- ISO/IEC 42001: контроли доступа к данным и системам ИИ.
- EU AI Act: Art. 10 (data governance), логирование доступа для high-risk.
- OWASP LLM: LLM02, LLM06.
- NIST AI RMF: Govern/Map — контроль данных и доступа.
Лаба и артефакт
Настроить Qdrant с payload-ACL и acl_version для «Ковчега», OIDC через Keycloak, pre-filter по tenant_id + groups, OpenFGA-модель отношений + batch_check в пути ретрива, webhook-синхронизацию отзыва прав, OPA на выходе. Тест: два пользователя с разными правами задают один вопрос из вступления — сравнить выдачу; отозвать доступ и замерить время реконсиляции.
Артефакт: FGA-модель + rego-политики + тест-матрица «роль × документ → доступ» + метрика лага синхронизации.
Чеклист зрелости
- L1: единый индекс с фильтром по
tenant_id. - L2: pre-filter по группам из JWT + FGA post-check по источнику, OPA на выходе.
- L3: событийная синхронизация ACL (минуты), защита от агрегации и утечки через цитаты, ABAC-контекст, аудит каждого доступа.
Источники
- Document-Level RBAC for RAG (2026 guide, Truto)
- Fine-Grained Authorization for RAG with OpenFGA (Auth0)
- The Right Approach to Authorization in RAG (Oso)
- RAG with Access Control (Pinecone)
Если ваш RAG отдаёт ответ, который пользователь не мог получить в источнике напрямую, — у вас не умный ассистент, а дыра в доступе, замаскированная под чат.