Skip to main content

sz_orm_core/
data_permission.rs

1//! 数据权限拦截器(Data Permission Interceptor)
2//!
3//! 对应文档 6.8 节改进项 25(数据权限拦截器)。
4//!
5//! # 核心概念
6//!
7//! - **PermissionContext**:权限上下文(当前用户、租户、部门)
8//! - **PermissionRule**:数据权限规则 trait(每个规则返回一个 WHERE 子句片段)
9//! - **DataPermissionInterceptor**:拦截器,注册多个规则并应用到 SQL
10//! - 内置规则:`TenantIsolation`、`OwnerOnly`、`DepartmentScope`、`CustomCondition`
11//!
12//! # 设计灵感
13//!
14//! - MyBatis-Plus `DataPermissionInterceptor`
15//! - Hibernate `@Filter` / `@FilterDef`
16//! - Rails `default_scope`
17//! - Spring Security `@PreAuthorize`
18//!
19//! # 使用示例
20//!
21//! ```no_run
22//! use sz_orm_core::data_permission::{
23//!     DataPermissionInterceptor, PermissionContext, TenantIsolation, OwnerOnly,
24//! };
25//!
26//! // 1. 创建拦截器
27//! let mut interceptor = DataPermissionInterceptor::new();
28//! interceptor.register(Box::new(TenantIsolation::new("tenant_id")));
29//! interceptor.register(Box::new(OwnerOnly::new("user_id")));
30//!
31//! // 2. 构建权限上下文
32//! let ctx = PermissionContext::new()
33//!     .with_user_id(100)
34//!     .with_tenant_id(5);
35//!
36//! // 3. 应用到 SQL(SELECT * FROM orders → SELECT * FROM orders WHERE tenant_id = 5 AND user_id = 100)
37//! let sql = interceptor.apply_to_select("SELECT * FROM orders", &ctx);
38//! ```
39
40use std::collections::HashMap;
41
42// ============================================================================
43// PermissionContext — 权限上下文
44// ============================================================================
45
46/// 权限上下文 — 当前请求的用户/租户/部门信息
47///
48/// 由调用方(通常是中间件)在请求开始时构建,并传递给 `DataPermissionInterceptor`。
49#[derive(Debug, Clone, Default)]
50pub struct PermissionContext {
51    /// 当前用户 ID
52    pub user_id: Option<i64>,
53    /// 当前租户 ID
54    pub tenant_id: Option<i64>,
55    /// 当前部门 ID
56    pub dept_id: Option<i64>,
57    /// 用户角色列表(用于角色级别的权限规则)
58    pub roles: Vec<String>,
59    /// 用户权限列表(细粒度权限码)
60    pub permissions: Vec<String>,
61    /// 扩展数据(如自定义字段值)
62    pub extras: HashMap<String, String>,
63}
64
65impl PermissionContext {
66    /// 创建空上下文
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// 设置用户 ID
72    pub fn with_user_id(mut self, user_id: i64) -> Self {
73        self.user_id = Some(user_id);
74        self
75    }
76
77    /// 设置租户 ID
78    pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
79        self.tenant_id = Some(tenant_id);
80        self
81    }
82
83    /// 设置部门 ID
84    pub fn with_dept_id(mut self, dept_id: i64) -> Self {
85        self.dept_id = Some(dept_id);
86        self
87    }
88
89    /// 设置角色列表
90    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
91        self.roles = roles;
92        self
93    }
94
95    /// 设置权限列表
96    pub fn with_permissions(mut self, perms: Vec<String>) -> Self {
97        self.permissions = perms;
98        self
99    }
100
101    /// 添加扩展数据
102    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
103        self.extras.insert(key.into(), value.into());
104        self
105    }
106
107    /// 检查是否拥有指定角色
108    pub fn has_role(&self, role: &str) -> bool {
109        self.roles.iter().any(|r| r == role)
110    }
111
112    /// 检查是否拥有指定权限
113    pub fn has_permission(&self, perm: &str) -> bool {
114        self.permissions.iter().any(|p| p == perm)
115    }
116
117    /// 是否为管理员(拥有 "admin" 角色)
118    pub fn is_admin(&self) -> bool {
119        self.has_role("admin") || self.has_role("super_admin")
120    }
121}
122
123// ============================================================================
124// PermissionRule — 数据权限规则 trait
125// ============================================================================
126
127/// 数据权限规则 trait
128///
129/// 每个规则负责生成一段 WHERE 子句(不含 `WHERE` 关键字本身),
130/// 拦截器会将多个规则的子句用 `AND` 连接,自动追加到原 SQL。
131///
132/// # 规则返回值
133/// - `Ok(Some(clause))`:应用规则,返回 WHERE 子句片段(如 `"tenant_id = 5"`)
134/// - `Ok(None)`:规则不适用(如管理员跳过、上下文缺失必要字段)
135/// - `Err(e)`:规则执行失败(如配置错误)
136pub trait PermissionRule: Send + Sync {
137    /// 规则名称(用于调试、日志)
138    fn name(&self) -> &'static str;
139
140    /// 生成 WHERE 子句片段
141    fn apply(&self, ctx: &PermissionContext) -> Result<Option<String>, PermissionError>;
142}
143
144// ============================================================================
145// PermissionError — 权限错误类型
146// ============================================================================
147
148/// 数据权限错误类型
149#[derive(Debug)]
150pub enum PermissionError {
151    /// 上下文缺失必要字段
152    MissingContext {
153        /// 字段名
154        field: &'static str,
155    },
156    /// 规则配置错误
157    ConfigError(String),
158    /// 不允许的操作(如非管理员尝试访问其他租户数据)
159    Forbidden(String),
160}
161
162impl std::fmt::Display for PermissionError {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            PermissionError::MissingContext { field } => {
166                write!(f, "Permission context missing field: `{}`", field)
167            }
168            PermissionError::ConfigError(msg) => {
169                write!(f, "Permission rule config error: {}", msg)
170            }
171            PermissionError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
172        }
173    }
174}
175
176impl std::error::Error for PermissionError {}
177
178/// 权限结果
179pub type PermissionResult<T> = Result<T, PermissionError>;
180
181// ============================================================================
182// 内置规则:TenantIsolation — 租户隔离
183// ============================================================================
184
185/// 租户隔离规则 — 自动追加 `tenant_id = ?` 条件
186///
187/// 对应 MyBatis-Plus `TenantLineHandler` / Hibernate `@TenantId`。
188///
189/// # 示例
190///
191/// ```
192/// use sz_orm_core::data_permission::{TenantIsolation, PermissionRule, PermissionContext};
193///
194/// let rule = TenantIsolation::new("tenant_id");
195/// let ctx = PermissionContext::new().with_tenant_id(5);
196/// let clause = rule.apply(&ctx).unwrap().unwrap();
197/// assert_eq!(clause, "tenant_id = 5");
198/// ```
199pub struct TenantIsolation {
200    /// 租户字段名(默认 "tenant_id")
201    pub field: &'static str,
202}
203
204impl TenantIsolation {
205    /// 创建租户隔离规则
206    pub fn new(field: &'static str) -> Self {
207        Self { field }
208    }
209
210    /// 使用默认字段名 "tenant_id"
211    pub fn default_field() -> Self {
212        Self::new("tenant_id")
213    }
214}
215
216impl PermissionRule for TenantIsolation {
217    fn name(&self) -> &'static str {
218        "TenantIsolation"
219    }
220
221    fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
222        // 管理员跳过租户隔离(可访问所有租户数据)
223        if ctx.is_admin() {
224            return Ok(None);
225        }
226        match ctx.tenant_id {
227            Some(tid) => Ok(Some(format!("{} = {}", self.field, tid))),
228            None => Err(PermissionError::MissingContext { field: "tenant_id" }),
229        }
230    }
231}
232
233// ============================================================================
234// 内置规则:OwnerOnly — 仅所有者可访问
235// ============================================================================
236
237/// 仅所有者可访问规则 — 自动追加 `user_id = ?` 条件
238///
239/// 对应 Rails `current_user` scope / Spring Security `@PostFilter`。
240///
241/// # 示例
242///
243/// ```
244/// use sz_orm_core::data_permission::{OwnerOnly, PermissionRule, PermissionContext};
245///
246/// let rule = OwnerOnly::new("user_id");
247/// let ctx = PermissionContext::new().with_user_id(100);
248/// let clause = rule.apply(&ctx).unwrap().unwrap();
249/// assert_eq!(clause, "user_id = 100");
250/// ```
251pub struct OwnerOnly {
252    /// 所有者字段名(默认 "user_id")
253    pub field: &'static str,
254}
255
256impl OwnerOnly {
257    /// 创建所有者规则
258    pub fn new(field: &'static str) -> Self {
259        Self { field }
260    }
261
262    /// 使用默认字段名 "user_id"
263    pub fn default_field() -> Self {
264        Self::new("user_id")
265    }
266}
267
268impl PermissionRule for OwnerOnly {
269    fn name(&self) -> &'static str {
270        "OwnerOnly"
271    }
272
273    fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
274        // 管理员可访问所有数据
275        if ctx.is_admin() {
276            return Ok(None);
277        }
278        match ctx.user_id {
279            Some(uid) => Ok(Some(format!("{} = {}", self.field, uid))),
280            None => Err(PermissionError::MissingContext { field: "user_id" }),
281        }
282    }
283}
284
285// ============================================================================
286// 内置规则:DepartmentScope — 部门范围
287// ============================================================================
288
289/// 部门范围规则 — 自动追加 `dept_id IN (...)` 或 `dept_id = ?` 条件
290///
291/// 对应 Spring Security `@DepartmentScope` / MyBatis-Plus `dept_id in (...)`。
292///
293/// # 示例
294///
295/// ```
296/// use sz_orm_core::data_permission::{DepartmentScope, PermissionRule, PermissionContext};
297///
298/// let rule = DepartmentScope::new("dept_id");
299/// let ctx = PermissionContext::new().with_dept_id(3);
300/// let clause = rule.apply(&ctx).unwrap().unwrap();
301/// assert_eq!(clause, "dept_id = 3");
302/// ```
303pub struct DepartmentScope {
304    /// 部门字段名(默认 "dept_id")
305    pub field: &'static str,
306    /// 子部门 ID 列表(如部门树展开后所有子孙部门)
307    pub include_sub_depts: Vec<i64>,
308}
309
310impl DepartmentScope {
311    /// 创建部门范围规则
312    pub fn new(field: &'static str) -> Self {
313        Self {
314            field,
315            include_sub_depts: Vec::new(),
316        }
317    }
318
319    /// 使用默认字段名 "dept_id"
320    pub fn default_field() -> Self {
321        Self::new("dept_id")
322    }
323
324    /// 包含子部门
325    pub fn with_sub_depts(mut self, depts: Vec<i64>) -> Self {
326        self.include_sub_depts = depts;
327        self
328    }
329}
330
331impl PermissionRule for DepartmentScope {
332    fn name(&self) -> &'static str {
333        "DepartmentScope"
334    }
335
336    fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
337        if ctx.is_admin() {
338            return Ok(None);
339        }
340        match ctx.dept_id {
341            Some(did) => {
342                if self.include_sub_depts.is_empty() {
343                    Ok(Some(format!("{} = {}", self.field, did)))
344                } else {
345                    // dept_id IN (did, sub1, sub2, ...)
346                    let mut all_depts = vec![did];
347                    all_depts.extend(self.include_sub_depts.iter().copied());
348                    let list = all_depts
349                        .iter()
350                        .map(|d| d.to_string())
351                        .collect::<Vec<_>>()
352                        .join(", ");
353                    Ok(Some(format!("{} IN ({})", self.field, list)))
354                }
355            }
356            None => Err(PermissionError::MissingContext { field: "dept_id" }),
357        }
358    }
359}
360
361// ============================================================================
362// 内置规则:CustomCondition — 自定义条件
363// ============================================================================
364
365/// 条件生成闭包类型
366pub type ConditionGenerator = Box<dyn Fn(&PermissionContext) -> Option<String> + Send + Sync>;
367
368/// 自定义条件规则 — 通过闭包动态生成 WHERE 子句
369///
370/// 适用于业务特定的权限逻辑(如"只能查看自己创建的草稿状态订单")。
371///
372/// # 安全警告
373///
374/// 闭包返回的 String 会**直接拼接**到 SQL 中,调用方必须确保内容来源可信,
375/// **严禁将用户输入直接拼接**到返回的字符串中(否则会引入 SQL 注入风险)。
376/// 若需要使用用户输入,应使用参数化查询(`?` 占位符 + bind 参数)。
377///
378/// # 示例
379///
380/// ```
381/// use sz_orm_core::data_permission::{CustomCondition, PermissionRule, PermissionContext};
382///
383/// let rule = CustomCondition::new("status_filter", |ctx| {
384///     if ctx.is_admin() {
385///         None
386///     } else {
387///         Some("status != 'draft'".to_string())
388///     }
389/// });
390/// let ctx = PermissionContext::new().with_user_id(1);
391/// let clause = rule.apply(&ctx).unwrap().unwrap();
392/// assert_eq!(clause, "status != 'draft'");
393/// ```
394pub struct CustomCondition {
395    /// 规则名称
396    pub name_str: &'static str,
397    /// 条件生成闭包
398    pub generator: ConditionGenerator,
399}
400
401impl CustomCondition {
402    /// 创建自定义条件规则
403    pub fn new(
404        name: &'static str,
405        generator: impl Fn(&PermissionContext) -> Option<String> + Send + Sync + 'static,
406    ) -> Self {
407        Self {
408            name_str: name,
409            generator: Box::new(generator),
410        }
411    }
412}
413
414impl PermissionRule for CustomCondition {
415    fn name(&self) -> &'static str {
416        self.name_str
417    }
418
419    fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
420        Ok((self.generator)(ctx))
421    }
422}
423
424// ============================================================================
425// DataPermissionInterceptor — 数据权限拦截器
426// ============================================================================
427
428/// 数据权限拦截器 — 注册多个规则并应用到 SQL
429///
430/// # 工作流程
431///
432/// 1. 调用方注册多个 `PermissionRule`
433/// 2. 在执行 SQL 前,调用 `apply_to_select` / `apply_to_update` / `apply_to_delete`
434/// 3. 拦截器按注册顺序依次调用每个 `PermissionRule.apply()`
435/// 4. 将所有非 None 的子句用 `AND` 连接,追加到原 SQL 的 WHERE 子句
436///
437/// # 示例
438///
439/// ```
440/// use sz_orm_core::data_permission::{
441///     DataPermissionInterceptor, PermissionContext, TenantIsolation, OwnerOnly,
442/// };
443///
444/// let mut interceptor = DataPermissionInterceptor::new();
445/// interceptor.register(Box::new(TenantIsolation::default_field()));
446/// interceptor.register(Box::new(OwnerOnly::default_field()));
447///
448/// let ctx = PermissionContext::new().with_user_id(100).with_tenant_id(5);
449/// let sql = interceptor.apply_to_select("SELECT * FROM orders", &ctx).unwrap();
450/// assert!(sql.contains("WHERE"));
451/// assert!(sql.contains("tenant_id = 5"));
452/// assert!(sql.contains("user_id = 100"));
453/// ```
454pub struct DataPermissionInterceptor {
455    rules: Vec<Box<dyn PermissionRule>>,
456}
457
458impl DataPermissionInterceptor {
459    /// 创建空拦截器
460    pub fn new() -> Self {
461        Self { rules: Vec::new() }
462    }
463
464    /// 注册规则
465    pub fn register(&mut self, rule: Box<dyn PermissionRule>) {
466        self.rules.push(rule);
467    }
468
469    /// 已注册规则数量
470    pub fn count(&self) -> usize {
471        self.rules.len()
472    }
473
474    /// 列出所有规则名称
475    pub fn names(&self) -> Vec<&'static str> {
476        self.rules.iter().map(|r| r.name()).collect()
477    }
478
479    /// 收集所有规则的 WHERE 子句(按注册顺序,跳过 None)
480    pub fn collect_clauses(&self, ctx: &PermissionContext) -> PermissionResult<Vec<String>> {
481        let mut clauses = Vec::new();
482        for rule in &self.rules {
483            if let Some(clause) = rule.apply(ctx)? {
484                if !clause.trim().is_empty() {
485                    clauses.push(clause);
486                }
487            }
488        }
489        Ok(clauses)
490    }
491
492    /// 将规则应用到 SELECT 语句
493    ///
494    /// - 原 SQL 无 WHERE 子句:追加 `WHERE clause1 AND clause2 ...`
495    /// - 原 SQL 有 WHERE 子句:在 WHERE 后追加 `(原条件) AND (clause1 AND clause2 ...)`
496    pub fn apply_to_select(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
497        let clauses = self.collect_clauses(ctx)?;
498        if clauses.is_empty() {
499            return Ok(sql.to_string());
500        }
501        Ok(append_where_clauses(sql, &clauses))
502    }
503
504    /// 将规则应用到 UPDATE 语句
505    pub fn apply_to_update(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
506        // 复用 SELECT 的逻辑(WHERE 子句追加)
507        self.apply_to_select(sql, ctx)
508    }
509
510    /// 将规则应用到 DELETE 语句
511    pub fn apply_to_delete(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
512        self.apply_to_select(sql, ctx)
513    }
514}
515
516impl Default for DataPermissionInterceptor {
517    fn default() -> Self {
518        Self::new()
519    }
520}
521
522// ============================================================================
523// 辅助函数:append_where_clauses — 追加 WHERE 子句
524// ============================================================================
525
526/// 将权限子句追加到 SQL 的 WHERE 部分
527///
528/// - 若 SQL 无 WHERE:追加 ` WHERE clauses`
529/// - 若 SQL 已有 WHERE:在 WHERE 后追加 ` AND (clauses)`
530/// - 若 SQL 已有 GROUP BY/ORDER BY/LIMIT:在它们之前追加
531pub fn append_where_clauses(sql: &str, clauses: &[String]) -> String {
532    if clauses.is_empty() {
533        return sql.to_string();
534    }
535
536    let combined = clauses.join(" AND ");
537    let upper = sql.to_uppercase();
538
539    // 查找 WHERE 关键字位置(独立词)
540    let where_pos = find_keyword(&upper, "WHERE");
541
542    // 查找其他可能的关键字位置(用于在 GROUP BY/ORDER BY/LIMIT 前插入)
543    let group_by_pos = find_keyword(&upper, "GROUP BY");
544    let order_by_pos = find_keyword(&upper, "ORDER BY");
545    let limit_pos = find_keyword(&upper, "LIMIT");
546    let having_pos = find_keyword(&upper, "HAVING");
547
548    // 找到最早出现的"末尾关键字"
549    let end_pos = [group_by_pos, order_by_pos, limit_pos, having_pos]
550        .iter()
551        .filter_map(|x| *x)
552        .min();
553
554    if let Some(wp) = where_pos {
555        // 已有 WHERE 子句
556        let insert_pos = end_pos.unwrap_or(sql.len());
557        let before = &sql[..wp + 5]; // 包含 "WHERE"
558        let existing_clause = &sql[wp + 5..insert_pos];
559        let after = &sql[insert_pos..];
560
561        // 在 WHERE 后追加 (existing) AND (combined)
562        let trimmed_existing = existing_clause.trim();
563        if trimmed_existing.is_empty() {
564            format!("{} {}{}", before, combined, after)
565        } else {
566            format!(
567                "{} ({} ) AND ({}){}",
568                before, trimmed_existing, combined, after
569            )
570        }
571    } else {
572        // 无 WHERE 子句
573        let insert_pos = end_pos.unwrap_or(sql.len());
574        let before = &sql[..insert_pos];
575        let after = &sql[insert_pos..];
576        let trimmed = before.trim_end();
577        let sep = if trimmed.is_empty() { "" } else { " " };
578        format!("{}{}WHERE {}{}", trimmed, sep, combined, after)
579    }
580}
581
582/// 在 SQL 中查找指定关键字的位置(独立词匹配,大小写不敏感)
583///
584/// v0.2.2 修复 H-2:增加括号深度跟踪,仅在 depth=0(顶层)匹配关键字,
585/// 避免误匹配子查询中的 WHERE/LIMIT/GROUP BY 等关键字。
586///
587/// 例如 `SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)`
588/// 中,外层 SELECT 的 LIMIT 应被忽略,只匹配 depth=0 处的关键字。
589fn find_keyword(sql: &str, keyword: &str) -> Option<usize> {
590    let upper_sql = sql.to_uppercase();
591    let kw_upper = keyword.to_uppercase();
592    let kw_len = kw_upper.len();
593    if kw_len == 0 || upper_sql.len() < kw_len {
594        return None;
595    }
596
597    let bytes = upper_sql.as_bytes();
598    let kw_bytes = kw_upper.as_bytes();
599
600    let mut i = 0;
601    let mut depth: i32 = 0; // 括号深度跟踪
602    while i + kw_len <= bytes.len() {
603        let b = bytes[i];
604        // 跟踪括号深度
605        if b == b'(' {
606            depth += 1;
607            i += 1;
608            continue;
609        }
610        if b == b')' {
611            if depth > 0 {
612                depth -= 1;
613            }
614            i += 1;
615            continue;
616        }
617        // 仅在 depth=0 时匹配关键字
618        if depth == 0 && &bytes[i..i + kw_len] == kw_bytes {
619            // 检查前一个字符是否为单词边界
620            let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
621            // 检查后一个字符是否为单词边界
622            let next_idx = i + kw_len;
623            let next_ok = next_idx >= bytes.len()
624                || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
625            if prev_ok && next_ok {
626                return Some(i);
627            }
628        }
629        i += 1;
630    }
631    None
632}
633
634// ============================================================================
635// 单元测试
636// ============================================================================
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    // ===== PermissionContext 测试 =====
643
644    #[test]
645    fn test_permission_context_builders() {
646        let ctx = PermissionContext::new()
647            .with_user_id(100)
648            .with_tenant_id(5)
649            .with_dept_id(3)
650            .with_roles(vec!["user".to_string()])
651            .with_permissions(vec!["read".to_string()])
652            .with_extra("region", "cn");
653
654        assert_eq!(ctx.user_id, Some(100));
655        assert_eq!(ctx.tenant_id, Some(5));
656        assert_eq!(ctx.dept_id, Some(3));
657        assert!(ctx.has_role("user"));
658        assert!(!ctx.has_role("admin"));
659        assert!(ctx.has_permission("read"));
660        assert_eq!(ctx.extras.get("region"), Some(&"cn".to_string()));
661    }
662
663    #[test]
664    fn test_permission_context_is_admin() {
665        let admin_ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
666        assert!(admin_ctx.is_admin());
667
668        let super_admin_ctx = PermissionContext::new().with_roles(vec!["super_admin".to_string()]);
669        assert!(super_admin_ctx.is_admin());
670
671        let user_ctx = PermissionContext::new().with_roles(vec!["user".to_string()]);
672        assert!(!user_ctx.is_admin());
673    }
674
675    // ===== TenantIsolation 测试 =====
676
677    #[test]
678    fn test_tenant_isolation_applies() {
679        let rule = TenantIsolation::default_field();
680        let ctx = PermissionContext::new().with_tenant_id(5);
681        let clause = rule.apply(&ctx).unwrap().unwrap();
682        assert_eq!(clause, "tenant_id = 5");
683    }
684
685    #[test]
686    fn test_tenant_isolation_skips_admin() {
687        let rule = TenantIsolation::default_field();
688        let ctx = PermissionContext::new()
689            .with_tenant_id(5)
690            .with_roles(vec!["admin".to_string()]);
691        let clause = rule.apply(&ctx).unwrap();
692        assert!(clause.is_none());
693    }
694
695    #[test]
696    fn test_tenant_isolation_missing_context() {
697        let rule = TenantIsolation::default_field();
698        let ctx = PermissionContext::new();
699        let result = rule.apply(&ctx);
700        assert!(matches!(
701            result,
702            Err(PermissionError::MissingContext { field }) if field == "tenant_id"
703        ));
704    }
705
706    #[test]
707    fn test_tenant_isolation_custom_field() {
708        let rule = TenantIsolation::new("org_id");
709        let ctx = PermissionContext::new().with_tenant_id(99);
710        let clause = rule.apply(&ctx).unwrap().unwrap();
711        assert_eq!(clause, "org_id = 99");
712    }
713
714    // ===== OwnerOnly 测试 =====
715
716    #[test]
717    fn test_owner_only_applies() {
718        let rule = OwnerOnly::default_field();
719        let ctx = PermissionContext::new().with_user_id(100);
720        let clause = rule.apply(&ctx).unwrap().unwrap();
721        assert_eq!(clause, "user_id = 100");
722    }
723
724    #[test]
725    fn test_owner_only_skips_admin() {
726        let rule = OwnerOnly::default_field();
727        let ctx = PermissionContext::new()
728            .with_user_id(100)
729            .with_roles(vec!["admin".to_string()]);
730        let clause = rule.apply(&ctx).unwrap();
731        assert!(clause.is_none());
732    }
733
734    #[test]
735    fn test_owner_only_missing_context() {
736        let rule = OwnerOnly::default_field();
737        let ctx = PermissionContext::new();
738        let result = rule.apply(&ctx);
739        assert!(matches!(
740            result,
741            Err(PermissionError::MissingContext { field }) if field == "user_id"
742        ));
743    }
744
745    // ===== DepartmentScope 测试 =====
746
747    #[test]
748    fn test_department_scope_simple() {
749        let rule = DepartmentScope::default_field();
750        let ctx = PermissionContext::new().with_dept_id(3);
751        let clause = rule.apply(&ctx).unwrap().unwrap();
752        assert_eq!(clause, "dept_id = 3");
753    }
754
755    #[test]
756    fn test_department_scope_with_sub_depts() {
757        let rule = DepartmentScope::default_field().with_sub_depts(vec![10, 11, 12]);
758        let ctx = PermissionContext::new().with_dept_id(3);
759        let clause = rule.apply(&ctx).unwrap().unwrap();
760        assert_eq!(clause, "dept_id IN (3, 10, 11, 12)");
761    }
762
763    #[test]
764    fn test_department_scope_skips_admin() {
765        let rule = DepartmentScope::default_field();
766        let ctx = PermissionContext::new()
767            .with_dept_id(3)
768            .with_roles(vec!["admin".to_string()]);
769        let clause = rule.apply(&ctx).unwrap();
770        assert!(clause.is_none());
771    }
772
773    // ===== CustomCondition 测试 =====
774
775    #[test]
776    fn test_custom_condition_returns_clause() {
777        let rule = CustomCondition::new("draft_filter", |ctx| {
778            if ctx.is_admin() {
779                None
780            } else {
781                Some("status != 'draft'".to_string())
782            }
783        });
784        let ctx = PermissionContext::new().with_user_id(1);
785        let clause = rule.apply(&ctx).unwrap().unwrap();
786        assert_eq!(clause, "status != 'draft'");
787    }
788
789    #[test]
790    fn test_custom_condition_skips_admin() {
791        let rule = CustomCondition::new("draft_filter", |ctx| {
792            if ctx.is_admin() {
793                None
794            } else {
795                Some("status != 'draft'".to_string())
796            }
797        });
798        let ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
799        let clause = rule.apply(&ctx).unwrap();
800        assert!(clause.is_none());
801    }
802
803    // ===== DataPermissionInterceptor 测试 =====
804
805    #[test]
806    fn test_interceptor_no_rules_returns_original_sql() {
807        let interceptor = DataPermissionInterceptor::new();
808        let ctx = PermissionContext::new().with_user_id(1);
809        let sql = interceptor
810            .apply_to_select("SELECT * FROM users", &ctx)
811            .unwrap();
812        assert_eq!(sql, "SELECT * FROM users");
813    }
814
815    #[test]
816    fn test_interceptor_single_rule_no_where() {
817        let mut interceptor = DataPermissionInterceptor::new();
818        interceptor.register(Box::new(TenantIsolation::default_field()));
819
820        let ctx = PermissionContext::new().with_tenant_id(5);
821        let sql = interceptor
822            .apply_to_select("SELECT * FROM orders", &ctx)
823            .unwrap();
824        assert!(sql.contains("WHERE tenant_id = 5"));
825    }
826
827    #[test]
828    fn test_interceptor_multiple_rules_no_where() {
829        let mut interceptor = DataPermissionInterceptor::new();
830        interceptor.register(Box::new(TenantIsolation::default_field()));
831        interceptor.register(Box::new(OwnerOnly::default_field()));
832
833        let ctx = PermissionContext::new().with_tenant_id(5).with_user_id(100);
834        let sql = interceptor
835            .apply_to_select("SELECT * FROM orders", &ctx)
836            .unwrap();
837        assert!(sql.contains("tenant_id = 5"));
838        assert!(sql.contains("user_id = 100"));
839        assert!(sql.contains("AND"));
840    }
841
842    #[test]
843    fn test_interceptor_appends_to_existing_where() {
844        let mut interceptor = DataPermissionInterceptor::new();
845        interceptor.register(Box::new(TenantIsolation::default_field()));
846
847        let ctx = PermissionContext::new().with_tenant_id(5);
848        let sql = interceptor
849            .apply_to_select("SELECT * FROM orders WHERE status = 'active'", &ctx)
850            .unwrap();
851        // 应保留原 WHERE 条件并追加
852        assert!(sql.contains("status = 'active'"));
853        assert!(sql.contains("tenant_id = 5"));
854        assert!(sql.contains("AND"));
855    }
856
857    #[test]
858    fn test_interceptor_admin_skips_all_rules() {
859        let mut interceptor = DataPermissionInterceptor::new();
860        interceptor.register(Box::new(TenantIsolation::default_field()));
861        interceptor.register(Box::new(OwnerOnly::default_field()));
862
863        let ctx = PermissionContext::new()
864            .with_tenant_id(5)
865            .with_user_id(100)
866            .with_roles(vec!["admin".to_string()]);
867
868        let sql = interceptor
869            .apply_to_select("SELECT * FROM orders", &ctx)
870            .unwrap();
871        // 管理员跳过所有规则,SQL 不变
872        assert_eq!(sql, "SELECT * FROM orders");
873    }
874
875    #[test]
876    fn test_interceptor_apply_to_update() {
877        let mut interceptor = DataPermissionInterceptor::new();
878        interceptor.register(Box::new(TenantIsolation::default_field()));
879
880        let ctx = PermissionContext::new().with_tenant_id(5);
881        let sql = interceptor
882            .apply_to_update("UPDATE orders SET status = 'shipped' WHERE id = 1", &ctx)
883            .unwrap();
884        assert!(sql.contains("id = 1"));
885        assert!(sql.contains("tenant_id = 5"));
886    }
887
888    #[test]
889    fn test_interceptor_apply_to_delete() {
890        let mut interceptor = DataPermissionInterceptor::new();
891        interceptor.register(Box::new(OwnerOnly::default_field()));
892
893        let ctx = PermissionContext::new().with_user_id(100);
894        let sql = interceptor
895            .apply_to_delete("DELETE FROM orders WHERE id = 1", &ctx)
896            .unwrap();
897        assert!(sql.contains("id = 1"));
898        assert!(sql.contains("user_id = 100"));
899    }
900
901    #[test]
902    fn test_interceptor_count_and_names() {
903        let mut interceptor = DataPermissionInterceptor::new();
904        assert_eq!(interceptor.count(), 0);
905        interceptor.register(Box::new(TenantIsolation::default_field()));
906        interceptor.register(Box::new(OwnerOnly::default_field()));
907        assert_eq!(interceptor.count(), 2);
908        let names = interceptor.names();
909        assert!(names.contains(&"TenantIsolation"));
910        assert!(names.contains(&"OwnerOnly"));
911    }
912
913    // ===== append_where_clauses 测试 =====
914
915    #[test]
916    fn test_append_where_no_existing_where_no_clauses() {
917        let sql = append_where_clauses("SELECT * FROM users", &[]);
918        assert_eq!(sql, "SELECT * FROM users");
919    }
920
921    #[test]
922    fn test_append_where_no_existing_where_with_clauses() {
923        let sql = append_where_clauses("SELECT * FROM users", &["tenant_id = 5".to_string()]);
924        assert_eq!(sql, "SELECT * FROM users WHERE tenant_id = 5");
925    }
926
927    #[test]
928    fn test_append_where_existing_where_with_clauses() {
929        let sql = append_where_clauses(
930            "SELECT * FROM users WHERE id = 1",
931            &["tenant_id = 5".to_string()],
932        );
933        assert!(sql.contains("id = 1"));
934        assert!(sql.contains("tenant_id = 5"));
935        assert!(sql.contains("AND"));
936    }
937
938    #[test]
939    fn test_append_where_inserts_before_group_by() {
940        let sql = append_where_clauses(
941            "SELECT * FROM users GROUP BY dept_id",
942            &["tenant_id = 5".to_string()],
943        );
944        // WHERE 应在 GROUP BY 之前
945        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
946        let group_by_idx = sql.to_uppercase().find("GROUP BY").unwrap();
947        assert!(where_idx < group_by_idx);
948    }
949
950    #[test]
951    fn test_append_where_inserts_before_order_by() {
952        let sql = append_where_clauses(
953            "SELECT * FROM users ORDER BY id",
954            &["tenant_id = 5".to_string()],
955        );
956        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
957        let order_by_idx = sql.to_uppercase().find("ORDER BY").unwrap();
958        assert!(where_idx < order_by_idx);
959    }
960
961    #[test]
962    fn test_append_where_inserts_before_limit() {
963        let sql = append_where_clauses(
964            "SELECT * FROM users LIMIT 10",
965            &["tenant_id = 5".to_string()],
966        );
967        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
968        let limit_idx = sql.to_uppercase().find("LIMIT").unwrap();
969        assert!(where_idx < limit_idx);
970    }
971
972    // ===== PermissionError Display 测试 =====
973
974    #[test]
975    fn test_permission_error_display_missing_context() {
976        let e = PermissionError::MissingContext { field: "user_id" };
977        let s = format!("{}", e);
978        assert!(s.contains("user_id"));
979        assert!(s.contains("missing"));
980    }
981
982    #[test]
983    fn test_permission_error_display_config_error() {
984        let e = PermissionError::ConfigError("invalid rule".to_string());
985        let s = format!("{}", e);
986        assert!(s.contains("invalid rule"));
987    }
988
989    #[test]
990    fn test_permission_error_display_forbidden() {
991        let e = PermissionError::Forbidden("cross-tenant access".to_string());
992        let s = format!("{}", e);
993        assert!(s.contains("Forbidden"));
994        assert!(s.contains("cross-tenant access"));
995    }
996
997    // ===== find_keyword 测试 =====
998
999    #[test]
1000    fn test_find_keyword_basic() {
1001        // "SELECT * FROM users WHERE id = 1" — WHERE 在位置 20
1002        assert_eq!(
1003            find_keyword("SELECT * FROM users WHERE id = 1", "WHERE"),
1004            Some(20)
1005        );
1006    }
1007
1008    #[test]
1009    fn test_find_keyword_not_found() {
1010        assert_eq!(find_keyword("SELECT * FROM users", "WHERE"), None);
1011    }
1012
1013    #[test]
1014    fn test_find_keyword_word_boundary() {
1015        // 不应匹配字段名中的子串
1016        assert_eq!(find_keyword("SELECT somewhere FROM t", "WHERE"), None);
1017    }
1018
1019    #[test]
1020    fn test_find_keyword_case_insensitive() {
1021        // "select * from t where id = 1" — where 在位置 16
1022        assert_eq!(
1023            find_keyword("select * from t where id = 1", "WHERE"),
1024            Some(16)
1025        );
1026    }
1027
1028    // ===== v0.2.2 修复 H-2:括号深度跟踪测试 =====
1029
1030    #[test]
1031    fn test_find_keyword_skips_subquery_where() {
1032        // 子查询中的 WHERE 不应被匹配(外层 WHERE 在位置 20)
1033        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs WHERE level = 1)";
1034        let pos = find_keyword(sql, "WHERE").unwrap();
1035        // 应该匹配外层 WHERE(位置 20),而不是子查询中的 WHERE
1036        assert_eq!(pos, 20);
1037        // 验证返回的位置确实是外层 WHERE
1038        assert_eq!(&sql[pos..pos + 5], "WHERE");
1039    }
1040
1041    #[test]
1042    fn test_find_keyword_skips_subquery_limit() {
1043        // 子查询中的 LIMIT 不应被匹配
1044        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)";
1045        // 外层没有 LIMIT,应返回 None
1046        assert_eq!(find_keyword(sql, "LIMIT"), None);
1047    }
1048
1049    #[test]
1050    fn test_find_keyword_finds_outer_limit() {
1051        // 外层 LIMIT 应被匹配,子查询中的 LIMIT 应被忽略
1052        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5) LIMIT 10";
1053        let pos = find_keyword(sql, "LIMIT").unwrap();
1054        // 应该匹配外层 LIMIT(位置 60),而不是子查询中的 LIMIT(位置 47)
1055        assert_eq!(&sql[pos..pos + 5], "LIMIT");
1056        // 确保不是子查询的 LIMIT
1057        assert!(pos > 50, "should match outer LIMIT, got pos={}", pos);
1058    }
1059
1060    #[test]
1061    fn test_find_keyword_skips_nested_parentheses() {
1062        // 多层嵌套括号
1063        let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM (SELECT * FROM t2 WHERE x = 1) sub)";
1064        let pos = find_keyword(sql, "WHERE").unwrap();
1065        // 应该匹配外层 WHERE(位置 16)
1066        assert_eq!(&sql[pos..pos + 5], "WHERE");
1067        assert_eq!(pos, 16);
1068    }
1069
1070    #[test]
1071    fn test_find_keyword_unbalanced_parentheses_safe() {
1072        // 不平衡括号不应导致 panic(depth > 0 时继续,但不影响安全性)
1073        let sql = "SELECT * FROM t WHERE id = 1)";
1074        // 应仍能匹配 WHERE
1075        assert!(find_keyword(sql, "WHERE").is_some());
1076    }
1077
1078    // ===== 默认 Default 测试 =====
1079
1080    #[test]
1081    fn test_interceptor_default_is_empty() {
1082        let i = DataPermissionInterceptor::default();
1083        assert_eq!(i.count(), 0);
1084    }
1085}