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        // SAFETY: combined 来自 PermissionRule::apply 内部生成(OwnerOnly/DepartmentScope 等),值为 format!("{} = {}", field, uid) 形式,field 为 &'static str,uid 为 i64,非用户输入
579        format!("{}{}WHERE {}{}", trimmed, sep, combined, after)
580    }
581}
582
583/// 在 SQL 中查找指定关键字的位置(独立词匹配,大小写不敏感)
584///
585/// v0.2.2 修复 H-2:增加括号深度跟踪,仅在 depth=0(顶层)匹配关键字,
586/// 避免误匹配子查询中的 WHERE/LIMIT/GROUP BY 等关键字。
587///
588/// 例如 `SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)`
589/// 中,外层 SELECT 的 LIMIT 应被忽略,只匹配 depth=0 处的关键字。
590fn find_keyword(sql: &str, keyword: &str) -> Option<usize> {
591    let upper_sql = sql.to_uppercase();
592    let kw_upper = keyword.to_uppercase();
593    let kw_len = kw_upper.len();
594    if kw_len == 0 || upper_sql.len() < kw_len {
595        return None;
596    }
597
598    let bytes = upper_sql.as_bytes();
599    let kw_bytes = kw_upper.as_bytes();
600
601    let mut i = 0;
602    let mut depth: i32 = 0; // 括号深度跟踪
603    while i + kw_len <= bytes.len() {
604        let b = bytes[i];
605        // 跟踪括号深度
606        if b == b'(' {
607            depth += 1;
608            i += 1;
609            continue;
610        }
611        if b == b')' {
612            if depth > 0 {
613                depth -= 1;
614            }
615            i += 1;
616            continue;
617        }
618        // 仅在 depth=0 时匹配关键字
619        if depth == 0 && &bytes[i..i + kw_len] == kw_bytes {
620            // 检查前一个字符是否为单词边界
621            let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
622            // 检查后一个字符是否为单词边界
623            let next_idx = i + kw_len;
624            let next_ok = next_idx >= bytes.len()
625                || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
626            if prev_ok && next_ok {
627                return Some(i);
628            }
629        }
630        i += 1;
631    }
632    None
633}
634
635// ============================================================================
636// 单元测试
637// ============================================================================
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    // ===== PermissionContext 测试 =====
644
645    #[test]
646    fn test_permission_context_builders() {
647        let ctx = PermissionContext::new()
648            .with_user_id(100)
649            .with_tenant_id(5)
650            .with_dept_id(3)
651            .with_roles(vec!["user".to_string()])
652            .with_permissions(vec!["read".to_string()])
653            .with_extra("region", "cn");
654
655        assert_eq!(ctx.user_id, Some(100));
656        assert_eq!(ctx.tenant_id, Some(5));
657        assert_eq!(ctx.dept_id, Some(3));
658        assert!(ctx.has_role("user"));
659        assert!(!ctx.has_role("admin"));
660        assert!(ctx.has_permission("read"));
661        assert_eq!(ctx.extras.get("region"), Some(&"cn".to_string()));
662    }
663
664    #[test]
665    fn test_permission_context_is_admin() {
666        let admin_ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
667        assert!(admin_ctx.is_admin());
668
669        let super_admin_ctx = PermissionContext::new().with_roles(vec!["super_admin".to_string()]);
670        assert!(super_admin_ctx.is_admin());
671
672        let user_ctx = PermissionContext::new().with_roles(vec!["user".to_string()]);
673        assert!(!user_ctx.is_admin());
674    }
675
676    // ===== TenantIsolation 测试 =====
677
678    #[test]
679    fn test_tenant_isolation_applies() {
680        let rule = TenantIsolation::default_field();
681        let ctx = PermissionContext::new().with_tenant_id(5);
682        let clause = rule.apply(&ctx).unwrap().unwrap();
683        assert_eq!(clause, "tenant_id = 5");
684    }
685
686    #[test]
687    fn test_tenant_isolation_skips_admin() {
688        let rule = TenantIsolation::default_field();
689        let ctx = PermissionContext::new()
690            .with_tenant_id(5)
691            .with_roles(vec!["admin".to_string()]);
692        let clause = rule.apply(&ctx).unwrap();
693        assert!(clause.is_none());
694    }
695
696    #[test]
697    fn test_tenant_isolation_missing_context() {
698        let rule = TenantIsolation::default_field();
699        let ctx = PermissionContext::new();
700        let result = rule.apply(&ctx);
701        assert!(matches!(
702            result,
703            Err(PermissionError::MissingContext { field }) if field == "tenant_id"
704        ));
705    }
706
707    #[test]
708    fn test_tenant_isolation_custom_field() {
709        let rule = TenantIsolation::new("org_id");
710        let ctx = PermissionContext::new().with_tenant_id(99);
711        let clause = rule.apply(&ctx).unwrap().unwrap();
712        assert_eq!(clause, "org_id = 99");
713    }
714
715    // ===== OwnerOnly 测试 =====
716
717    #[test]
718    fn test_owner_only_applies() {
719        let rule = OwnerOnly::default_field();
720        let ctx = PermissionContext::new().with_user_id(100);
721        let clause = rule.apply(&ctx).unwrap().unwrap();
722        assert_eq!(clause, "user_id = 100");
723    }
724
725    #[test]
726    fn test_owner_only_skips_admin() {
727        let rule = OwnerOnly::default_field();
728        let ctx = PermissionContext::new()
729            .with_user_id(100)
730            .with_roles(vec!["admin".to_string()]);
731        let clause = rule.apply(&ctx).unwrap();
732        assert!(clause.is_none());
733    }
734
735    #[test]
736    fn test_owner_only_missing_context() {
737        let rule = OwnerOnly::default_field();
738        let ctx = PermissionContext::new();
739        let result = rule.apply(&ctx);
740        assert!(matches!(
741            result,
742            Err(PermissionError::MissingContext { field }) if field == "user_id"
743        ));
744    }
745
746    // ===== DepartmentScope 测试 =====
747
748    #[test]
749    fn test_department_scope_simple() {
750        let rule = DepartmentScope::default_field();
751        let ctx = PermissionContext::new().with_dept_id(3);
752        let clause = rule.apply(&ctx).unwrap().unwrap();
753        assert_eq!(clause, "dept_id = 3");
754    }
755
756    #[test]
757    fn test_department_scope_with_sub_depts() {
758        let rule = DepartmentScope::default_field().with_sub_depts(vec![10, 11, 12]);
759        let ctx = PermissionContext::new().with_dept_id(3);
760        let clause = rule.apply(&ctx).unwrap().unwrap();
761        assert_eq!(clause, "dept_id IN (3, 10, 11, 12)");
762    }
763
764    #[test]
765    fn test_department_scope_skips_admin() {
766        let rule = DepartmentScope::default_field();
767        let ctx = PermissionContext::new()
768            .with_dept_id(3)
769            .with_roles(vec!["admin".to_string()]);
770        let clause = rule.apply(&ctx).unwrap();
771        assert!(clause.is_none());
772    }
773
774    // ===== CustomCondition 测试 =====
775
776    #[test]
777    fn test_custom_condition_returns_clause() {
778        let rule = CustomCondition::new("draft_filter", |ctx| {
779            if ctx.is_admin() {
780                None
781            } else {
782                Some("status != 'draft'".to_string())
783            }
784        });
785        let ctx = PermissionContext::new().with_user_id(1);
786        let clause = rule.apply(&ctx).unwrap().unwrap();
787        assert_eq!(clause, "status != 'draft'");
788    }
789
790    #[test]
791    fn test_custom_condition_skips_admin() {
792        let rule = CustomCondition::new("draft_filter", |ctx| {
793            if ctx.is_admin() {
794                None
795            } else {
796                Some("status != 'draft'".to_string())
797            }
798        });
799        let ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
800        let clause = rule.apply(&ctx).unwrap();
801        assert!(clause.is_none());
802    }
803
804    // ===== DataPermissionInterceptor 测试 =====
805
806    #[test]
807    fn test_interceptor_no_rules_returns_original_sql() {
808        let interceptor = DataPermissionInterceptor::new();
809        let ctx = PermissionContext::new().with_user_id(1);
810        let sql = interceptor
811            .apply_to_select("SELECT * FROM users", &ctx)
812            .unwrap();
813        assert_eq!(sql, "SELECT * FROM users");
814    }
815
816    #[test]
817    fn test_interceptor_single_rule_no_where() {
818        let mut interceptor = DataPermissionInterceptor::new();
819        interceptor.register(Box::new(TenantIsolation::default_field()));
820
821        let ctx = PermissionContext::new().with_tenant_id(5);
822        let sql = interceptor
823            .apply_to_select("SELECT * FROM orders", &ctx)
824            .unwrap();
825        assert!(sql.contains("WHERE tenant_id = 5"));
826    }
827
828    #[test]
829    fn test_interceptor_multiple_rules_no_where() {
830        let mut interceptor = DataPermissionInterceptor::new();
831        interceptor.register(Box::new(TenantIsolation::default_field()));
832        interceptor.register(Box::new(OwnerOnly::default_field()));
833
834        let ctx = PermissionContext::new().with_tenant_id(5).with_user_id(100);
835        let sql = interceptor
836            .apply_to_select("SELECT * FROM orders", &ctx)
837            .unwrap();
838        assert!(sql.contains("tenant_id = 5"));
839        assert!(sql.contains("user_id = 100"));
840        assert!(sql.contains("AND"));
841    }
842
843    #[test]
844    fn test_interceptor_appends_to_existing_where() {
845        let mut interceptor = DataPermissionInterceptor::new();
846        interceptor.register(Box::new(TenantIsolation::default_field()));
847
848        let ctx = PermissionContext::new().with_tenant_id(5);
849        let sql = interceptor
850            .apply_to_select("SELECT * FROM orders WHERE status = 'active'", &ctx)
851            .unwrap();
852        // 应保留原 WHERE 条件并追加
853        assert!(sql.contains("status = 'active'"));
854        assert!(sql.contains("tenant_id = 5"));
855        assert!(sql.contains("AND"));
856    }
857
858    #[test]
859    fn test_interceptor_admin_skips_all_rules() {
860        let mut interceptor = DataPermissionInterceptor::new();
861        interceptor.register(Box::new(TenantIsolation::default_field()));
862        interceptor.register(Box::new(OwnerOnly::default_field()));
863
864        let ctx = PermissionContext::new()
865            .with_tenant_id(5)
866            .with_user_id(100)
867            .with_roles(vec!["admin".to_string()]);
868
869        let sql = interceptor
870            .apply_to_select("SELECT * FROM orders", &ctx)
871            .unwrap();
872        // 管理员跳过所有规则,SQL 不变
873        assert_eq!(sql, "SELECT * FROM orders");
874    }
875
876    #[test]
877    fn test_interceptor_apply_to_update() {
878        let mut interceptor = DataPermissionInterceptor::new();
879        interceptor.register(Box::new(TenantIsolation::default_field()));
880
881        let ctx = PermissionContext::new().with_tenant_id(5);
882        let sql = interceptor
883            .apply_to_update("UPDATE orders SET status = 'shipped' WHERE id = 1", &ctx)
884            .unwrap();
885        assert!(sql.contains("id = 1"));
886        assert!(sql.contains("tenant_id = 5"));
887    }
888
889    #[test]
890    fn test_interceptor_apply_to_delete() {
891        let mut interceptor = DataPermissionInterceptor::new();
892        interceptor.register(Box::new(OwnerOnly::default_field()));
893
894        let ctx = PermissionContext::new().with_user_id(100);
895        let sql = interceptor
896            .apply_to_delete("DELETE FROM orders WHERE id = 1", &ctx)
897            .unwrap();
898        assert!(sql.contains("id = 1"));
899        assert!(sql.contains("user_id = 100"));
900    }
901
902    #[test]
903    fn test_interceptor_count_and_names() {
904        let mut interceptor = DataPermissionInterceptor::new();
905        assert_eq!(interceptor.count(), 0);
906        interceptor.register(Box::new(TenantIsolation::default_field()));
907        interceptor.register(Box::new(OwnerOnly::default_field()));
908        assert_eq!(interceptor.count(), 2);
909        let names = interceptor.names();
910        assert!(names.contains(&"TenantIsolation"));
911        assert!(names.contains(&"OwnerOnly"));
912    }
913
914    // ===== append_where_clauses 测试 =====
915
916    #[test]
917    fn test_append_where_no_existing_where_no_clauses() {
918        let sql = append_where_clauses("SELECT * FROM users", &[]);
919        assert_eq!(sql, "SELECT * FROM users");
920    }
921
922    #[test]
923    fn test_append_where_no_existing_where_with_clauses() {
924        let sql = append_where_clauses("SELECT * FROM users", &["tenant_id = 5".to_string()]);
925        assert_eq!(sql, "SELECT * FROM users WHERE tenant_id = 5");
926    }
927
928    #[test]
929    fn test_append_where_existing_where_with_clauses() {
930        let sql = append_where_clauses(
931            "SELECT * FROM users WHERE id = 1",
932            &["tenant_id = 5".to_string()],
933        );
934        assert!(sql.contains("id = 1"));
935        assert!(sql.contains("tenant_id = 5"));
936        assert!(sql.contains("AND"));
937    }
938
939    #[test]
940    fn test_append_where_inserts_before_group_by() {
941        let sql = append_where_clauses(
942            "SELECT * FROM users GROUP BY dept_id",
943            &["tenant_id = 5".to_string()],
944        );
945        // WHERE 应在 GROUP BY 之前
946        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
947        let group_by_idx = sql.to_uppercase().find("GROUP BY").unwrap();
948        assert!(where_idx < group_by_idx);
949    }
950
951    #[test]
952    fn test_append_where_inserts_before_order_by() {
953        let sql = append_where_clauses(
954            "SELECT * FROM users ORDER BY id",
955            &["tenant_id = 5".to_string()],
956        );
957        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
958        let order_by_idx = sql.to_uppercase().find("ORDER BY").unwrap();
959        assert!(where_idx < order_by_idx);
960    }
961
962    #[test]
963    fn test_append_where_inserts_before_limit() {
964        let sql = append_where_clauses(
965            "SELECT * FROM users LIMIT 10",
966            &["tenant_id = 5".to_string()],
967        );
968        let where_idx = sql.to_uppercase().find("WHERE").unwrap();
969        let limit_idx = sql.to_uppercase().find("LIMIT").unwrap();
970        assert!(where_idx < limit_idx);
971    }
972
973    // ===== PermissionError Display 测试 =====
974
975    #[test]
976    fn test_permission_error_display_missing_context() {
977        let e = PermissionError::MissingContext { field: "user_id" };
978        let s = format!("{}", e);
979        assert!(s.contains("user_id"));
980        assert!(s.contains("missing"));
981    }
982
983    #[test]
984    fn test_permission_error_display_config_error() {
985        let e = PermissionError::ConfigError("invalid rule".to_string());
986        let s = format!("{}", e);
987        assert!(s.contains("invalid rule"));
988    }
989
990    #[test]
991    fn test_permission_error_display_forbidden() {
992        let e = PermissionError::Forbidden("cross-tenant access".to_string());
993        let s = format!("{}", e);
994        assert!(s.contains("Forbidden"));
995        assert!(s.contains("cross-tenant access"));
996    }
997
998    // ===== find_keyword 测试 =====
999
1000    #[test]
1001    fn test_find_keyword_basic() {
1002        // "SELECT * FROM users WHERE id = 1" — WHERE 在位置 20
1003        assert_eq!(
1004            find_keyword("SELECT * FROM users WHERE id = 1", "WHERE"),
1005            Some(20)
1006        );
1007    }
1008
1009    #[test]
1010    fn test_find_keyword_not_found() {
1011        assert_eq!(find_keyword("SELECT * FROM users", "WHERE"), None);
1012    }
1013
1014    #[test]
1015    fn test_find_keyword_word_boundary() {
1016        // 不应匹配字段名中的子串
1017        assert_eq!(find_keyword("SELECT somewhere FROM t", "WHERE"), None);
1018    }
1019
1020    #[test]
1021    fn test_find_keyword_case_insensitive() {
1022        // "select * from t where id = 1" — where 在位置 16
1023        assert_eq!(
1024            find_keyword("select * from t where id = 1", "WHERE"),
1025            Some(16)
1026        );
1027    }
1028
1029    // ===== v0.2.2 修复 H-2:括号深度跟踪测试 =====
1030
1031    #[test]
1032    fn test_find_keyword_skips_subquery_where() {
1033        // 子查询中的 WHERE 不应被匹配(外层 WHERE 在位置 20)
1034        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs WHERE level = 1)";
1035        let pos = find_keyword(sql, "WHERE").unwrap();
1036        // 应该匹配外层 WHERE(位置 20),而不是子查询中的 WHERE
1037        assert_eq!(pos, 20);
1038        // 验证返回的位置确实是外层 WHERE
1039        assert_eq!(&sql[pos..pos + 5], "WHERE");
1040    }
1041
1042    #[test]
1043    fn test_find_keyword_skips_subquery_limit() {
1044        // 子查询中的 LIMIT 不应被匹配
1045        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)";
1046        // 外层没有 LIMIT,应返回 None
1047        assert_eq!(find_keyword(sql, "LIMIT"), None);
1048    }
1049
1050    #[test]
1051    fn test_find_keyword_finds_outer_limit() {
1052        // 外层 LIMIT 应被匹配,子查询中的 LIMIT 应被忽略
1053        let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5) LIMIT 10";
1054        let pos = find_keyword(sql, "LIMIT").unwrap();
1055        // 应该匹配外层 LIMIT(位置 60),而不是子查询中的 LIMIT(位置 47)
1056        assert_eq!(&sql[pos..pos + 5], "LIMIT");
1057        // 确保不是子查询的 LIMIT
1058        assert!(pos > 50, "should match outer LIMIT, got pos={}", pos);
1059    }
1060
1061    #[test]
1062    fn test_find_keyword_skips_nested_parentheses() {
1063        // 多层嵌套括号
1064        let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM (SELECT * FROM t2 WHERE x = 1) sub)";
1065        let pos = find_keyword(sql, "WHERE").unwrap();
1066        // 应该匹配外层 WHERE(位置 16)
1067        assert_eq!(&sql[pos..pos + 5], "WHERE");
1068        assert_eq!(pos, 16);
1069    }
1070
1071    #[test]
1072    fn test_find_keyword_unbalanced_parentheses_safe() {
1073        // 不平衡括号不应导致 panic(depth > 0 时继续,但不影响安全性)
1074        let sql = "SELECT * FROM t WHERE id = 1)";
1075        // 应仍能匹配 WHERE
1076        assert!(find_keyword(sql, "WHERE").is_some());
1077    }
1078
1079    // ===== 默认 Default 测试 =====
1080
1081    #[test]
1082    fn test_interceptor_default_is_empty() {
1083        let i = DataPermissionInterceptor::default();
1084        assert_eq!(i.count(), 0);
1085    }
1086}