Изоляция арендаторов
Как AACsearch гарантирует, что данные одной организации недоступны другой.
Изоляция арендаторов
AACsearch — мультиарендный. Один поисковый кластер обслуживает много организаций. Межарендных чтений не бывает. Ни через admin-ключ, ни через ошибочный фильтр, ни через подделанный scoped-токен. Эта страница объясняет, как эта гарантия обеспечивается и где ваша зона ответственности.
Иерархия
flowchart TD
Org["Организация<br/>(аккаунт клиента)"]
Proj["Проект<br/>(prod / staging / …)"]
Idx["Индекс<br/>(коллекция Typesense)"]
Doc["Документ<br/>(несёт ваш tenantId для прикладного scoping)"]
Keys["API-ключи<br/>(ограничены индексами внутри проекта)"]
Members["Участники · Биллинг · Аудит"]
Org --> Proj
Org --> Members
Proj --> Idx
Proj --> Keys
Idx --> Doc
classDef enforced fill:#1e293b,color:#fff,stroke:#0ea5e9,stroke-width:2px
classDef yours fill:#0f172a,color:#fde68a,stroke:#f59e0b,stroke-width:1px,stroke-dasharray:4
class Org,Proj,Idx,Keys,Members enforced
class Doc yoursПунктирный узел (Документ/tenantId) — ваш прикладной scoping, см. Scoped-токены. Сплошные узлы — enforced AACsearch.
| Уровень | Что это | Изоляция |
|---|---|---|
| Организация | Верхнеуровневый аккаунт клиента. | Жёсткая. Ключи, индексы, участники и биллинг привязаны к одной организации. |
| Проект | Группировка внутри организации (prod, staging). | Ключи принадлежат одному проекту. Кросс-проектные обращения отвергаются. |
| Индекс | Коллекция Typesense под одним поисковым корпусом. | Ключи могут быть ограничены подмножеством индексов проекта. |
| Tenant ID | Поле на уровне документа (ваше). | Ваше поле scoping (customerId, accountId). Принудительно через фильтр. |
Первые три уровня enforced AACsearch. Четвёртый — вашим фильтром, обычно через scoped-токен.
Как обеспечивается изоляция организации
В каждый запрос, который доходит до search-кода, передаётся объект verified, построенный из API-ключа:
{
organizationId: "org_abc",
projectId: "prj_xyz",
keyId: "key_…",
scopes: ["search"],
// …
}Внутри каждый вызов поиска передаёт tenantId: verified.organizationId в search-слой. Слой:
- Резолвит логическое имя индекса в organization-prefixed имя физической коллекции (например,
org_abc__products__v3). - Маршрутизирует запрос только в эту коллекцию.
- Возвращает результат — пути, по которому запрос может уйти в чужую коллекцию, нет.
Это Инвариант 5 в agents.md: каждый вызов передаёт tenantId: verified.organizationId. Кросс-org чтений не бывает. Баг, нарушающий это, — инцидент P0.
Как обеспечивается изоляция проекта и индекса
При создании ключа вы выбираете:
- Доступны ли ему все индексы проекта или конкретный список.
- Какие scopes разрешены.
На верификации проверяется, что запрошенный индекс — в allow-list ключа (если он непуст) и операция разрешена scopes. search-ключ при вызове index.delete отвергается до любого обращения к БД.
Как обеспечивается прикладная изоляция
Чтобы делить общий индекс между пользователями, кладите в документ поле — обычно tenantId, userId, accountId, companyId. Принуждение:
- Индексируйте это поле в каждом документе.
- Требуйте его в фильтре каждого запроса.
Надёжный способ «требовать» — выпускать scoped-токен с filterBy: "tenantId:=<value>". Серверный AND-комбайн делает так, что пользователь не сможет это обойти даже хитрым клиентским фильтром. Браузер просит, что хочет, — результат всегда пересечение с фильтром токена.
Браузер: stock:>0
Токен: tenantId:=org_abc
AND: tenantId:=org_abc && stock:>0Это Инвариант 4: scoped-токены сужают, никогда не расширяют.
Что пойдёт не так, если этого не делать
Частая ошибка — общий индекс между пользователями без scoped-токенов с расчётом на «приложение всегда пришлёт нужный фильтр». Любой путь, где фильтр пропущен или контролируется пользователем, обнажает все данные. AND-комбайн scoped-токенов и существует, чтобы баг приложения не приводил к этому.
Если в общем индексе обязаны жить данные нескольких арендаторов — всегда используйте scoped-токены. Если scoped-токены не подходят (например, поиск сугубо серверный), enforce фильтра в коде сервера и пишите тест, который падает при пропаже фильтра.
Сценарии, которые выглядят как кросс-org, но решаются иначе
- Одна компания, два staging. Заведите два проекта в одной организации либо два индекса с alias-ами. Кросс-org не нужен.
- Реселлер / агентство ведут несколько мерчантов. Каждый мерчант — отдельная организация. Сотрудники агентства — участники каждой. Дашборд переключает «активную организацию», и все вызовы идут с ключами этой org.
- Внутренний admin-консоль с агрегацией метрик. Агрегируйте из endpoint-ов аналитики каждой организации её собственным admin-ключом. Не обходите org-фильтр в поиске.
Если кейс действительно требует кросс-org чтений — это кастом, пишите на sales.
Проверка изоляции в собственных тестах
Когда строите фичу поверх AACsearch — пишите хотя бы один негативный тест:
test("user A cannot see user B's documents", async () => {
await indexAs(userA, { id: "1", tenantId: "tenant_a", title: "secret a" });
await indexAs(userB, { id: "2", tenantId: "tenant_b", title: "secret b" });
const tokenA = mintScopedTokenFor(userA); // filterBy: tenantId:=tenant_a
const res = await searchAs(tokenA, { q: "secret" });
expect(res.hits.map((h) => h.document.id)).toEqual(["1"]);
});Если такой тест когда-нибудь упадёт — это инцидент P0.
См. также
- Scoped-токены — механизм AND-фильтров
- API-ключи — org/project/index allow-list на ключе
- Регион хранения — изоляция уровня региона