Сотрудник клиента A спросил помощника о скидках. В ответе появилась сумма из договора клиента B. Модель ничего не взламывала: приложение сначала сделало глобальный vector search, а tenant_id попыталось проверить после получения top-10. Документы A просто не вошли в десятку.
Права доступа являются условием retrieval, а не фильтром перед показом ответа. То же относится к удалению, сроку действия и уровню конфиденциальности.
Граница данных
identity -> tenant membership -> document policy -> search candidates
-> prompt -> answerИдентификатор tenant берут из проверенной сессии или service credential, но не из произвольного поля запроса. В PostgreSQL полезно дублировать защиту Row-Level Security:
ALTER TABLE chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_chunks ON chunks
USING (tenant_id = current_setting('app.tenant_id')::uuid);Транзакция должна установить app.tenant_id из аутентифицированного principal. Сервисная роль, обходящая RLS, превращает защиту в декорацию.
async def search_for_user(db, principal, query_embedding):
async with db.transaction():
await db.execute(
"select set_config('app.tenant_id', $1, true)",
str(principal.tenant_id),
)
return await db.fetch("""
select id, document_id, text
from chunks
where visibility = 'tenant'
order by embedding <=> $1
limit 8
""", query_embedding)Для ACL сложнее tenant добавляют таблицу разрешений и фильтруют кандидатов в SQL. Индекс и объектное хранилище должны удаляться согласованно. Запись deleted_at в каталоге бесполезна, если старый чанк остаётся в поисковом индексе ещё сутки.
Что обычно ломают
- Принимают
tenant_idиз JSON и забывают сверить membership. - Ищут глобально, затем отбрасывают запрещённое. Это ухудшает и безопасность, и recall.
- Кэшируют ответ только по тексту вопроса, без tenant и версии ACL.
- Пишут полный prompt в trace, доступный общей группе инженеров.
- Индексируют подписанные URL, токены и персональные поля вместе с полезным текстом.
- Считают удаление документа завершённым до очистки чанков, кэшей и резервных копий по принятой политике.
Тест на изоляцию должен создавать одинаковые документы в двух tenant с разными секретными маркерами. Запрос от A не должен вернуть маркер B ни в ответе, ни в citations, логах и trace.
Правило главы
Авторизация происходит до ранжирования и сохраняется на всём пути данных. Если запрещённый фрагмент успел попасть в prompt, проверка ответа уже опоздала.
Практикум
Создайте два tenant, по три пользователя и документы с пересекающимся текстом. Реализуйте RLS, tenant-aware cache key и удаление из индекса. Напишите интеграционные тесты на подмену tenant_id, смену роли во время жизни кэша, удалённый документ и утечку маркера через observability.