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.
59 lines
2.3 KiB
Rust
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())
|
|
}
|