CI / Check (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
76 lines
2.5 KiB
Rust
76 lines
2.5 KiB
Rust
//! Database helpers shared across the workspace.
|
|
//!
|
|
//! `tenant_filter` returns the BSON filter that every query and update
|
|
//! against a tenant-scoped collection MUST include. Centralising it here
|
|
//! makes the rule grep-able and keeps query call-sites from accidentally
|
|
//! omitting it.
|
|
//!
|
|
//! Future work (M7.2+): each collection model grows a `tenant_id` field
|
|
//! and every `find` / `update_*` / `delete_*` call gets this filter
|
|
//! merged in. The migration to per-collection scoping is tracked
|
|
//! separately — this helper is the building block.
|
|
|
|
use bson::{doc, Document};
|
|
|
|
use crate::TenantContext;
|
|
|
|
/// Returns `{ "tenant_id": <ctx.tenant_id> }`. Merge this into every
|
|
/// query filter against a tenant-scoped collection.
|
|
///
|
|
/// Use [`tenant_filter_merge`] when you need to combine it with other
|
|
/// query conditions — it preserves both halves without overwriting.
|
|
pub fn tenant_filter(ctx: &TenantContext) -> Document {
|
|
doc! { "tenant_id": &ctx.tenant_id }
|
|
}
|
|
|
|
/// Returns the tenant filter merged with caller-supplied conditions.
|
|
/// The tenant_id always wins on key conflict — callers cannot
|
|
/// accidentally override the scoping.
|
|
pub fn tenant_filter_merge(ctx: &TenantContext, mut extra: Document) -> Document {
|
|
extra.insert("tenant_id", &ctx.tenant_id);
|
|
extra
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::TenantStatus;
|
|
|
|
fn ctx() -> TenantContext {
|
|
TenantContext {
|
|
tenant_id: "t-abc".to_string(),
|
|
tenant_slug: "acme".to_string(),
|
|
org_roles: vec![],
|
|
products: vec![],
|
|
plan: "starter".to_string(),
|
|
status: TenantStatus::Active,
|
|
user_id: "u-1".to_string(),
|
|
user_name: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn produces_tenant_id_filter() {
|
|
let f = tenant_filter(&ctx());
|
|
assert_eq!(f.get_str("tenant_id"), Ok("t-abc"));
|
|
assert_eq!(f.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_preserves_extra_conditions() {
|
|
let extra = doc! { "status": "open", "severity": "high" };
|
|
let f = tenant_filter_merge(&ctx(), extra);
|
|
assert_eq!(f.get_str("tenant_id"), Ok("t-abc"));
|
|
assert_eq!(f.get_str("status"), Ok("open"));
|
|
assert_eq!(f.get_str("severity"), Ok("high"));
|
|
}
|
|
|
|
#[test]
|
|
fn merge_overrides_caller_tenant_id() {
|
|
let extra = doc! { "tenant_id": "evil-other", "status": "open" };
|
|
let f = tenant_filter_merge(&ctx(), extra);
|
|
assert_eq!(f.get_str("tenant_id"), Ok("t-abc"));
|
|
assert_eq!(f.get_str("status"), Ok("open"));
|
|
}
|
|
}
|