AACsearch
Безопасность и соответствие

Изоляция арендаторов

Как 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-слой. Слой:

  1. Резолвит логическое имя индекса в organization-prefixed имя физической коллекции (например, org_abc__products__v3).
  2. Маршрутизирует запрос только в эту коллекцию.
  3. Возвращает результат — пути, по которому запрос может уйти в чужую коллекцию, нет.

Это Инвариант 5 в agents.md: каждый вызов передаёт tenantId: verified.organizationId. Кросс-org чтений не бывает. Баг, нарушающий это, — инцидент P0.

Как обеспечивается изоляция проекта и индекса

При создании ключа вы выбираете:

  • Доступны ли ему все индексы проекта или конкретный список.
  • Какие scopes разрешены.

На верификации проверяется, что запрошенный индекс — в allow-list ключа (если он непуст) и операция разрешена scopes. search-ключ при вызове index.delete отвергается до любого обращения к БД.

Как обеспечивается прикладная изоляция

Чтобы делить общий индекс между пользователями, кладите в документ поле — обычно tenantId, userId, accountId, companyId. Принуждение:

  1. Индексируйте это поле в каждом документе.
  2. Требуйте его в фильтре каждого запроса.

Надёжный способ «требовать» — выпускать 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.

См. также

On this page