1use crate::value::Value;
10
11#[derive(Debug, Clone)]
17pub struct ParameterizedCondition {
18 pub sql_fragment: String,
20 pub params: Vec<Value>,
22}
23
24impl ParameterizedCondition {
25 pub fn new(sql_fragment: impl Into<String>, params: Vec<Value>) -> Self {
27 Self {
28 sql_fragment: sql_fragment.into(),
29 params,
30 }
31 }
32
33 pub fn literal(sql_fragment: impl Into<String>) -> Self {
35 Self {
36 sql_fragment: sql_fragment.into(),
37 params: Vec::new(),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct Principal {
45 pub tenant_id: i64,
47 pub roles: Vec<String>,
49}
50
51impl Principal {
52 pub fn new(tenant_id: i64, roles: Vec<String>) -> Self {
54 Self { tenant_id, roles }
55 }
56
57 pub fn has_role(&self, role: &str) -> bool {
59 self.roles.iter().any(|r| r == role)
60 }
61}
62
63#[derive(Debug, Clone)]
67pub struct RowLevelSecurityPolicy {
68 pub table: String,
70 pub filter_condition: ParameterizedCondition,
72 pub principal: Principal,
74}
75
76impl RowLevelSecurityPolicy {
77 pub fn new(
79 table: impl Into<String>,
80 filter_condition: ParameterizedCondition,
81 principal: Principal,
82 ) -> Self {
83 Self {
84 table: table.into(),
85 filter_condition,
86 principal,
87 }
88 }
89}
90
91pub use sz_orm_masking::MaskingRule as MaskingFunction;
95
96#[derive(Debug, Clone)]
101pub struct PermissionPredicate {
102 pub applicable_roles: Option<Vec<String>>,
104 pub exempt_roles: Vec<String>,
106}
107
108impl PermissionPredicate {
109 pub fn all() -> Self {
111 Self {
112 applicable_roles: None,
113 exempt_roles: Vec::new(),
114 }
115 }
116
117 pub fn for_roles(roles: Vec<String>) -> Self {
119 Self {
120 applicable_roles: Some(roles),
121 exempt_roles: Vec::new(),
122 }
123 }
124
125 pub fn with_exempt(mut self, roles: Vec<String>) -> Self {
127 self.exempt_roles = roles;
128 self
129 }
130
131 pub fn applies_to(&self, roles: &[String]) -> bool {
133 if roles.iter().any(|r| self.exempt_roles.contains(r)) {
135 return false;
136 }
137 match &self.applicable_roles {
139 None => true,
140 Some(applicable) => roles.iter().any(|r| applicable.contains(r)),
141 }
142 }
143}
144
145impl Default for PermissionPredicate {
146 fn default() -> Self {
147 Self::all()
148 }
149}
150
151#[derive(Debug, Clone)]
155pub struct ColumnMaskingRule {
156 pub table: String,
158 pub column: String,
160 pub masking_function: MaskingFunction,
162 pub applicable_permissions: PermissionPredicate,
164}
165
166impl ColumnMaskingRule {
167 pub fn new(
169 table: impl Into<String>,
170 column: impl Into<String>,
171 masking_function: MaskingFunction,
172 applicable_permissions: PermissionPredicate,
173 ) -> Self {
174 Self {
175 table: table.into(),
176 column: column.into(),
177 masking_function,
178 applicable_permissions,
179 }
180 }
181
182 pub fn mask(&self, value: &str) -> String {
184 sz_orm_masking::DataMasker::apply(&self.masking_function, value)
185 }
186
187 pub fn applies_to(&self, roles: &[String]) -> bool {
189 self.applicable_permissions.applies_to(roles)
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum TenantAuditOperation {
198 ContextSet,
200 ContextSwitch,
202 CrossTenantDenied,
204 RowLevelFiltered,
206 ColumnMasked,
208}
209
210impl std::fmt::Display for TenantAuditOperation {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 Self::ContextSet => write!(f, "context_set"),
214 Self::ContextSwitch => write!(f, "context_switch"),
215 Self::CrossTenantDenied => write!(f, "cross_tenant_denied"),
216 Self::RowLevelFiltered => write!(f, "row_level_filtered"),
217 Self::ColumnMasked => write!(f, "column_masked"),
218 }
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum AuditResult {
225 Success,
227 Denied,
229}
230
231impl std::fmt::Display for AuditResult {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Self::Success => write!(f, "success"),
235 Self::Denied => write!(f, "denied"),
236 }
237 }
238}
239
240#[derive(Debug, Clone)]
244pub struct TenantAuditContext {
245 pub tenant_id: i64,
247 pub operation: TenantAuditOperation,
249 pub timestamp: i64,
251 pub result: AuditResult,
253 pub detail: String,
255}
256
257impl TenantAuditContext {
258 pub fn new(
260 tenant_id: i64,
261 operation: TenantAuditOperation,
262 result: AuditResult,
263 detail: impl Into<String>,
264 ) -> Self {
265 Self {
266 tenant_id,
267 operation,
268 timestamp: chrono::Utc::now().timestamp(),
269 result,
270 detail: detail.into(),
271 }
272 }
273
274 pub fn log_to(&self, auditor: &sz_orm_audit::SqlAuditor) {
276 let ctx = sz_orm_audit::SqlAuditContext {
277 sql: format!(
278 "[tenant={}] {} {} {}",
279 self.tenant_id, self.operation, self.result, self.detail
280 ),
281 user: format!("tenant_{}", self.tenant_id),
282 timestamp: self.timestamp,
283 };
284 auditor.log(&ctx);
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_parameterized_condition_new() {
294 let cond = ParameterizedCondition::new("department_id = $1", vec![Value::I64(10)]);
295 assert_eq!(cond.sql_fragment, "department_id = $1");
296 assert_eq!(cond.params.len(), 1);
297 }
298
299 #[test]
300 fn test_parameterized_condition_literal() {
301 let cond = ParameterizedCondition::literal("is_active = true");
302 assert_eq!(cond.sql_fragment, "is_active = true");
303 assert!(cond.params.is_empty());
304 }
305
306 #[test]
307 fn test_principal_has_role() {
308 let principal = Principal::new(42, vec!["admin".to_string(), "manager".to_string()]);
309 assert!(principal.has_role("admin"));
310 assert!(principal.has_role("manager"));
311 assert!(!principal.has_role("employee"));
312 }
313
314 #[test]
315 fn test_row_level_security_policy() {
316 let policy = RowLevelSecurityPolicy::new(
317 "orders",
318 ParameterizedCondition::new("department_id = $1", vec![Value::I64(10)]),
319 Principal::new(42, vec!["manager".to_string()]),
320 );
321 assert_eq!(policy.table, "orders");
322 assert_eq!(policy.filter_condition.sql_fragment, "department_id = $1");
323 assert_eq!(policy.principal.tenant_id, 42);
324 }
325
326 #[test]
327 fn test_permission_predicate_all() {
328 let pred = PermissionPredicate::all();
329 let roles = vec!["employee".to_string()];
330 assert!(pred.applies_to(&roles));
331 }
332
333 #[test]
334 fn test_permission_predicate_exempt() {
335 let pred = PermissionPredicate::all().with_exempt(vec!["admin".to_string()]);
336 let admin_roles = vec!["admin".to_string()];
337 let employee_roles = vec!["employee".to_string()];
338 assert!(!pred.applies_to(&admin_roles));
339 assert!(pred.applies_to(&employee_roles));
340 }
341
342 #[test]
343 fn test_permission_predicate_for_roles() {
344 let pred = PermissionPredicate::for_roles(vec!["employee".to_string()]);
345 let employee_roles = vec!["employee".to_string()];
346 let manager_roles = vec!["manager".to_string()];
347 assert!(pred.applies_to(&employee_roles));
348 assert!(!pred.applies_to(&manager_roles));
349 }
350
351 #[test]
352 fn test_column_masking_rule_mask() {
353 let rule = ColumnMaskingRule::new(
354 "users",
355 "phone",
356 MaskingFunction::Phone,
357 PermissionPredicate::all(),
358 );
359 let masked = rule.mask("13812345678");
360 assert!(masked.starts_with("138"));
361 assert!(masked.ends_with("5678"));
362 assert!(masked.contains('*'));
363 }
364
365 #[test]
366 fn test_column_masking_rule_applies_to() {
367 let rule = ColumnMaskingRule::new(
368 "users",
369 "phone",
370 MaskingFunction::Phone,
371 PermissionPredicate::all().with_exempt(vec!["admin".to_string()]),
372 );
373 assert!(!rule.applies_to(&["admin".to_string()]));
374 assert!(rule.applies_to(&["employee".to_string()]));
375 }
376
377 #[test]
378 fn test_tenant_audit_operation_display() {
379 assert_eq!(TenantAuditOperation::ContextSet.to_string(), "context_set");
380 assert_eq!(
381 TenantAuditOperation::CrossTenantDenied.to_string(),
382 "cross_tenant_denied"
383 );
384 }
385
386 #[test]
387 fn test_audit_result_display() {
388 assert_eq!(AuditResult::Success.to_string(), "success");
389 assert_eq!(AuditResult::Denied.to_string(), "denied");
390 }
391
392 #[test]
393 fn test_tenant_audit_context_new() {
394 let ctx = TenantAuditContext::new(
395 42,
396 TenantAuditOperation::ContextSet,
397 AuditResult::Success,
398 "tenant context set for tenant 42",
399 );
400 assert_eq!(ctx.tenant_id, 42);
401 assert_eq!(ctx.operation, TenantAuditOperation::ContextSet);
402 assert_eq!(ctx.result, AuditResult::Success);
403 }
404
405 #[test]
406 fn test_tenant_audit_context_log_to() {
407 let auditor = sz_orm_audit::SqlAuditor::new();
408 let ctx = TenantAuditContext::new(
409 42,
410 TenantAuditOperation::ContextSet,
411 AuditResult::Success,
412 "test",
413 );
414 ctx.log_to(&auditor);
415 let logs = auditor.get_logs();
416 assert_eq!(logs.len(), 1);
417 assert!(logs[0].sql.contains("tenant=42"));
418 assert!(logs[0].sql.contains("context_set"));
419 }
420}