Skip to main content

sz_orm_core/
tenant_security.rs

1//! 多租户安全策略:行级安全 + 列级脱敏 + 多租户审计
2//!
3//! 本模块在 `multi-tenant-enhanced` feature gate 下导出,提供:
4//! - [`ParameterizedCondition`] — 参数化过滤条件(禁止 SQL 字符串拼接)
5//! - [`Principal`] + [`RowLevelSecurityPolicy`] — 行级安全策略(部门级/角色级细粒度)
6//! - [`MaskingFunction`] + [`PermissionPredicate`] + [`ColumnMaskingRule`] — 列级脱敏规则
7//! - [`TenantAuditOperation`] + [`AuditResult`] + [`TenantAuditContext`] — 多租户审计
8
9use crate::value::Value;
10
11// ─── M1-T6:行级安全策略 ───────────────────────────────────────────
12
13/// 参数化过滤条件(SQL 片段含占位符 + 参数值列表)
14///
15/// 禁止 SQL 字符串拼接,所有条件必须通过参数化绑定传递。
16#[derive(Debug, Clone)]
17pub struct ParameterizedCondition {
18    /// SQL 片段,含 `$1` / `$2` 等占位符(如 `"department_id = $1"`)
19    pub sql_fragment: String,
20    /// 参数值列表,与占位符按位置对应
21    pub params: Vec<Value>,
22}
23
24impl ParameterizedCondition {
25    /// 创建新的参数化条件
26    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    /// 无参数的条件(如 `"is_active = true"`)
34    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/// 权限主体(租户 ID + 角色列表)
43#[derive(Debug, Clone)]
44pub struct Principal {
45    /// 租户 ID
46    pub tenant_id: i64,
47    /// 角色列表(如 `"admin"` / `"manager"` / `"employee"`)
48    pub roles: Vec<String>,
49}
50
51impl Principal {
52    /// 创建新的权限主体
53    pub fn new(tenant_id: i64, roles: Vec<String>) -> Self {
54        Self { tenant_id, roles }
55    }
56
57    /// 检查是否拥有指定角色
58    pub fn has_role(&self, role: &str) -> bool {
59        self.roles.iter().any(|r| r == role)
60    }
61}
62
63/// 行级安全策略(扩展既有 `AccessRule`,提供部门级/角色级细粒度过滤)
64///
65/// 策略由服务端定义,不可被客户端篡改。
66#[derive(Debug, Clone)]
67pub struct RowLevelSecurityPolicy {
68    /// 表名
69    pub table: String,
70    /// 参数化过滤条件(如 `"department_id = $1"`,非 SQL 字符串拼接)
71    pub filter_condition: ParameterizedCondition,
72    /// 权限主体(租户 ID + 角色)
73    pub principal: Principal,
74}
75
76impl RowLevelSecurityPolicy {
77    /// 创建新的行级安全策略
78    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
91// ─── M1-T7:列级脱敏规则 ───────────────────────────────────────────
92
93/// 脱敏函数枚举(复用既有 `sz_orm_masking::MaskingRule`)
94pub use sz_orm_masking::MaskingRule as MaskingFunction;
95
96/// 权限谓词(描述未授权租户/角色条件)
97///
98/// 当 `applicable_roles` 为 `None` 时,所有未在 `exempt_roles` 中的角色都适用脱敏。
99/// 当 `applicable_roles` 为 `Some(roles)` 时,仅指定角色适用脱敏。
100#[derive(Debug, Clone)]
101pub struct PermissionPredicate {
102    /// 适用脱敏的角色列表(`None` 表示所有角色除 `exempt_roles` 外)
103    pub applicable_roles: Option<Vec<String>>,
104    /// 豁免脱敏的角色列表(如 `"admin"` 可见原始值)
105    pub exempt_roles: Vec<String>,
106}
107
108impl PermissionPredicate {
109    /// 所有角色都适用脱敏(无豁免)
110    pub fn all() -> Self {
111        Self {
112            applicable_roles: None,
113            exempt_roles: Vec::new(),
114        }
115    }
116
117    /// 仅指定角色适用脱敏
118    pub fn for_roles(roles: Vec<String>) -> Self {
119        Self {
120            applicable_roles: Some(roles),
121            exempt_roles: Vec::new(),
122        }
123    }
124
125    /// 豁免指定角色(如 admin 可见原始值)
126    pub fn with_exempt(mut self, roles: Vec<String>) -> Self {
127        self.exempt_roles = roles;
128        self
129    }
130
131    /// 判断给定角色列表是否适用脱敏
132    pub fn applies_to(&self, roles: &[String]) -> bool {
133        // 先检查豁免
134        if roles.iter().any(|r| self.exempt_roles.contains(r)) {
135            return false;
136        }
137        // 再检查适用范围
138        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/// 列级脱敏规则
152///
153/// ORM 层强制执行,不可绕过。未配置脱敏规则的敏感列默认拒绝读取(安全优先)。
154#[derive(Debug, Clone)]
155pub struct ColumnMaskingRule {
156    /// 表名
157    pub table: String,
158    /// 列名
159    pub column: String,
160    /// 脱敏函数
161    pub masking_function: MaskingFunction,
162    /// 适用权限(未授权租户/角色才脱敏)
163    pub applicable_permissions: PermissionPredicate,
164}
165
166impl ColumnMaskingRule {
167    /// 创建新的列级脱敏规则
168    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    /// 对给定值执行脱敏
183    pub fn mask(&self, value: &str) -> String {
184        sz_orm_masking::DataMasker::apply(&self.masking_function, value)
185    }
186
187    /// 判断给定角色列表是否需要脱敏
188    pub fn applies_to(&self, roles: &[String]) -> bool {
189        self.applicable_permissions.applies_to(roles)
190    }
191}
192
193// ─── M1-T8:多租户审计 ─────────────────────────────────────────────
194
195/// 多租户审计操作类型
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum TenantAuditOperation {
198    /// 上下文设置
199    ContextSet,
200    /// 租户切换
201    ContextSwitch,
202    /// 跨租户访问拒绝
203    CrossTenantDenied,
204    /// 行级安全过滤
205    RowLevelFiltered,
206    /// 列级脱敏执行
207    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/// 审计结果
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum AuditResult {
225    /// 成功
226    Success,
227    /// 拒绝
228    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/// 多租户审计上下文
241///
242/// 审计日志含租户 ID + 操作 + 时间 + 结果,日志不可篡改(追加写入)。
243#[derive(Debug, Clone)]
244pub struct TenantAuditContext {
245    /// 租户 ID
246    pub tenant_id: i64,
247    /// 操作类型
248    pub operation: TenantAuditOperation,
249    /// 时间戳(Unix 秒)
250    pub timestamp: i64,
251    /// 结果(成功/拒绝)
252    pub result: AuditResult,
253    /// 详情(如被拒绝的表名、被脱敏的列名等)
254    pub detail: String,
255}
256
257impl TenantAuditContext {
258    /// 创建新的审计上下文
259    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    /// 记入审计日志(调用既有 `SqlAuditor::log`)
275    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}