Files
cairnobs/search/src/config.rs
T
jcoffey-dev ba2276aa1a 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.
2026-08-13 23:16:22 -07:00

59 lines
2.3 KiB
Rust

use anyhow::{Context, Result};
use std::path::PathBuf;
use std::time::Duration;
/// All via environment variables, same convention as /ingest and /api —
/// no config file format for this service either.
pub struct Config {
pub grpc_listen_addr: String,
pub redpanda_brokers: Vec<String>,
pub redpanda_topic: String,
pub index_path: PathBuf,
pub offsets_path: PathBuf,
pub commit_interval: Duration,
/// Phase 4: per-tenant index directories live under here, one
/// subdirectory per tenant_id, opened on demand by
/// registry::IndexRegistry -- distinct from `index_path` above,
/// which stays the single shared index every ingest-written record
/// lands in regardless of tenant (see registry.rs's doc comment and
/// /docs/security/threat-model.md's ingest-tenancy caveat). Default
/// matches the path convention deploy/operator's Tenant controller
/// and enterprise/internal/rbacstore's seeded default data source
/// already assume (`/var/lib/sentry-search/tenants/<id>`).
pub tenants_index_path: PathBuf,
}
impl Config {
pub fn load() -> Result<Self> {
let commit_interval_ms: u64 = getenv("COMMIT_INTERVAL_MS", "2000")
.parse()
.context("COMMIT_INTERVAL_MS must be a number")?;
Ok(Self {
// Rust's SocketAddr parser needs a full address, unlike Go's
// net package (ingest/api's ":PORT" convention won't parse
// here).
grpc_listen_addr: getenv("GRPC_LISTEN_ADDR", "0.0.0.0:50052"),
redpanda_brokers: getenv("REDPANDA_BROKERS", "localhost:9092")
.split(',')
.map(str::to_string)
.collect(),
redpanda_topic: getenv("REDPANDA_TOPIC", "sentry.logs.raw"),
index_path: PathBuf::from(getenv("INDEX_PATH", "/var/lib/sentry-search/index")),
offsets_path: PathBuf::from(getenv(
"OFFSETS_PATH",
"/var/lib/sentry-search/offsets.json",
)),
commit_interval: Duration::from_millis(commit_interval_ms),
tenants_index_path: PathBuf::from(getenv(
"TENANTS_INDEX_PATH",
"/var/lib/sentry-search/tenants",
)),
})
}
}
fn getenv(key: &str, fallback: &str) -> String {
std::env::var(key).unwrap_or_else(|_| fallback.to_string())
}