Phase 4: real Tantivy per-tenant isolation (search/src/registry.rs, enterprise/internal/searchclient)

Closes the last named "isolation mechanism" gap: search.proto gains a
tenant_id field on SearchRequest; search/src/registry.rs's IndexRegistry
resolves it to an on-demand-opened, per-tenant Tantivy index (empty
tenant_id keeps today's single default index, so this is purely
additive); enterprise/internal/searchclient sets that field from the
authenticated request identity in ctx, mirroring chrunner's exact
fail-closed "never a parameter" shape. Wired into enterprise-api in
place of the shared api/searchclient.

Unlike the ClickHouse pieces from the previous two commits, this one is
genuinely verified end to end in this environment: Tantivy is an
embedded library, not a networked service, so both the Rust index
registry (cargo test, cargo clippy --all-targets -- -D warnings, both
clean) and the Go client (a real in-process gRPC server) could actually
run. registry.rs's tenant_index_is_isolated_from_default_and_other_tenants
seeds three real indices with the same term and confirms a tenant-scoped
search returns only that tenant's document -- item 3 of the isolation
design doc's verification plan, closed for real, not just written.

With both ClickHouse and Tantivy isolation now built, the single largest
remaining gap is no longer a missing mechanism: it's that nothing forces
or flags whether a deployment actually runs enterprise-api instead of
plain api, and that ingest itself has no tenant concept for either
storage engine (every record still lands in the one shared database/
index no matter what -- undesigned, not just unbuilt). Updated the
threat model, architecture doc, CLAUDE.md, and both READMEs accordingly.
This commit is contained in:
2026-08-13 23:16:22 -07:00
parent 1fab02abd5
commit ba2276aa1a
17 changed files with 696 additions and 168 deletions
+15 -9
View File
@@ -159,15 +159,21 @@ tenant/role from `tenant_memberships`) — genuinely verified, unlike the
ClickHouse pieces, via a real fake IdP that signs and verifies actual
RS256 tokens (`loginhandler_test.go`, all passing), though never tried
against a real external IdP or through a running `enterprise-auth`
container. Two things still keep this phase from being done: SAML login
(protocol wiring exists, no ACS handler calls it, following OIDC's now
-built pattern), and Tantivy/free-text queries have no per-tenant index
routing at all (`enterprise-api` closes the ClickHouse half of tenant
isolation, not the Tantivy half) — plus a deployment gap worth naming
explicitly: nothing yet forces or even flags whether a given deployment
is actually running the isolated binary (`enterprise-api`) versus the
plain single-tenant one (`api`); both still exist and nothing currently
prevents mixing them up. Full accounting:
container. Tantivy per-tenant index routing is now built too
(`search/src/registry.rs` + `enterprise/internal/searchclient`) —
**genuinely verified**, like the OIDC login flow: Tantivy is an embedded
library, not a networked service, so the isolation probe (three tenants,
same search term, scoped search returns only that tenant's document)
actually ran in this environment, no Docker needed. What still keeps
this phase from being done: SAML login (protocol wiring exists, no ACS
handler calls it, following OIDC's now-built pattern), ingest itself has
no tenant concept for either storage engine (every record lands in the
one shared ClickHouse database and Tantivy index no matter what —
undesigned, not just unbuilt), and a deployment-topology gap that's now
the single largest one: nothing yet forces or even flags whether a given
deployment is actually running the isolated binary (`enterprise-api`)
versus the plain single-tenant one (`api`); both still exist and nothing
currently prevents mixing them up. Full accounting:
`/docs/security/threat-model.md`; step-by-step verification procedure
(not yet run against a live cluster in this environment):
`/docs/phase-4-runbook.md`. The rest of this section describes the exit