1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! Multi-tenancy enforcement layer.
//!
//! Ensures all database queries are scoped to the requesting organization by
//! automatically injecting `org_id` filters into WHERE clauses. Supports both
//! the structured [`WhereClause`] AST (preferred, parameterized) and raw SQL
//! string injection as a fallback.
use serde_json::json;
use crate::db::where_clause::{WhereClause, WhereOperator};
/// Multi-tenancy enforcer for query scoping
///
/// Automatically adds `org_id` filtering to all database queries
/// to ensure strict tenant isolation at runtime.
#[derive(Debug, Clone)]
pub struct TenantEnforcer {
/// Current `org_id` for this request
org_id: Option<String>,
/// Enforce tenant scoping (require `org_id` for all queries)
require_tenant: bool,
}
impl TenantEnforcer {
/// Create a new tenant enforcer
#[must_use]
pub const fn new(org_id: Option<String>) -> Self {
Self {
org_id,
require_tenant: false,
}
}
/// Create with tenant requirement
#[must_use]
pub const fn with_requirement(org_id: Option<String>, require_tenant: bool) -> Self {
Self {
org_id,
require_tenant,
}
}
/// Check if request is tenant-scoped
#[must_use]
pub const fn is_tenant_scoped(&self) -> bool {
self.org_id.is_some()
}
/// Get the `org_id` for this request
#[must_use]
pub fn get_org_id(&self) -> Option<&str> {
self.org_id.as_deref()
}
/// Enforce tenant scoping on a WHERE clause
///
/// Automatically adds an `AND org_id = <org_id>` condition
/// to ensure all queries return only data for the current tenant.
///
/// # Arguments
/// * `where_clause` - User-provided WHERE clause
///
/// # Errors
///
/// Returns an error string if tenant enforcement is required but `org_id` is not set.
///
/// # Returns
/// * Modified WHERE clause with tenant filter added
/// * Or error if tenant enforcement is required but `org_id` not provided
pub fn enforce_tenant_scope(
&self,
where_clause: Option<&WhereClause>,
) -> Result<Option<WhereClause>, String> {
// Check if tenant enforcement is required
if self.require_tenant && self.org_id.is_none() {
return Err("Request must be tenant-scoped (missing org_id)".to_string());
}
// If no org_id, return original clause unchanged (public/unauthenticated)
let Some(org_id) = &self.org_id else {
return Ok(where_clause.cloned());
};
// Build org_id filter clause
let org_id_filter = WhereClause::Field {
path: vec!["org_id".to_string()],
operator: WhereOperator::Eq,
value: json!(org_id),
};
// Combine with user's WHERE clause
let enforced_clause = match where_clause {
None => org_id_filter,
Some(user_clause) => WhereClause::And(vec![user_clause.clone(), org_id_filter]),
};
Ok(Some(enforced_clause))
}
/// Enforce tenant scope for raw SQL queries
///
/// Adds WHERE `org_id` = '<`org_id`>' to raw SQL if needed.
/// This is a simpler approach for raw queries.
///
/// # Security
///
/// The `org_id` value is escaped to prevent SQL injection. Prefer using
/// `enforce_tenant_scope()` with `WhereClause` AST for parameterized queries.
///
/// # Arguments
/// * `sql` - Original SQL query
///
/// # Errors
///
/// Returns an error string if tenant enforcement is required but `org_id` is not set.
///
/// # Returns
/// * Modified SQL with tenant filter, or original if no `org_id`
pub fn enforce_tenant_scope_sql(&self, sql: &str) -> Result<String, String> {
// Check if tenant enforcement is required
if self.require_tenant && self.org_id.is_none() {
return Err("Request must be tenant-scoped (missing org_id)".to_string());
}
// If no org_id, return original SQL unchanged
let Some(org_id) = &self.org_id else {
return Ok(sql.to_string());
};
// SECURITY: Escape single quotes to prevent SQL injection via org_id
let escaped_org_id = org_id.replace('\'', "''");
// For raw SQL, we need to be careful about WHERE clause placement
let sql_upper = sql.to_uppercase();
// Add WHERE clause if none exists
let enforced_sql = if sql_upper.contains("WHERE") {
// Append to existing WHERE with AND
format!("{sql} AND org_id = '{escaped_org_id}'")
} else if sql_upper.contains("GROUP BY") {
// Insert before GROUP BY
let parts: Vec<&str> = sql.splitn(2, "GROUP BY").collect();
if parts.len() == 2 {
format!("{} WHERE org_id = '{}' GROUP BY {}", parts[0], escaped_org_id, parts[1])
} else {
sql.to_string()
}
} else if sql_upper.contains("ORDER BY") {
// Insert before ORDER BY
let parts: Vec<&str> = sql.splitn(2, "ORDER BY").collect();
if parts.len() == 2 {
format!("{} WHERE org_id = '{}' ORDER BY {}", parts[0], escaped_org_id, parts[1])
} else {
sql.to_string()
}
} else {
// Append at end
format!("{sql} WHERE org_id = '{escaped_org_id}'")
};
Ok(enforced_sql)
}
}