Skip to main content

sz_orm_core/
access_control.rs

1//! 行级和字段级权限控制
2//!
3//! 提供基于租户/用户的行级数据隔离和字段级访问控制
4
5use std::collections::{HashMap, HashSet};
6
7/// 权限规则
8#[derive(Debug, Clone)]
9pub struct AccessRule {
10    /// 表名
11    pub table: String,
12    /// 行级过滤条件(SQL WHERE 子句片段)
13    pub row_filter: Option<String>,
14    /// 允许查询的字段列表(None 表示允许所有字段)
15    pub allowed_columns: Option<HashSet<String>>,
16    /// 禁止查询的字段列表
17    pub denied_columns: HashSet<String>,
18}
19
20/// 访问控制上下文
21#[derive(Debug, Clone, Default)]
22pub struct AccessContext {
23    /// 当前租户 ID
24    pub tenant_id: Option<String>,
25    /// 当前用户 ID
26    pub user_id: Option<String>,
27    /// 角色列表
28    pub roles: Vec<String>,
29    /// 表级权限规则
30    rules: HashMap<String, AccessRule>,
31}
32
33impl AccessContext {
34    /// 创建新的访问控制上下文
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// 设置租户 ID
40    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
41        self.tenant_id = Some(tenant_id.into());
42        self
43    }
44
45    /// 设置用户 ID
46    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
47        self.user_id = Some(user_id.into());
48        self
49    }
50
51    /// 添加访问规则
52    pub fn add_rule(&mut self, rule: AccessRule) {
53        self.rules.insert(rule.table.clone(), rule);
54    }
55
56    /// 获取表的行级过滤条件
57    pub fn row_filter(&self, table: &str) -> Option<&str> {
58        self.rules.get(table).and_then(|r| r.row_filter.as_deref())
59    }
60
61    /// 检查字段是否允许查询
62    pub fn is_column_allowed(&self, table: &str, column: &str) -> bool {
63        if let Some(rule) = self.rules.get(table) {
64            if rule.denied_columns.contains(column) {
65                return false;
66            }
67            if let Some(ref allowed) = rule.allowed_columns {
68                return allowed.contains(column);
69            }
70        }
71        true
72    }
73
74    /// 过滤字段列表,返回允许查询的字段
75    pub fn filter_columns(&self, table: &str, columns: &[String]) -> Vec<String> {
76        columns
77            .iter()
78            .filter(|col| self.is_column_allowed(table, col))
79            .cloned()
80            .collect()
81    }
82}
83
84/// 行级权限构建器
85pub struct RowLevelSecurity {
86    context: AccessContext,
87}
88
89impl RowLevelSecurity {
90    pub fn new(context: AccessContext) -> Self {
91        Self { context }
92    }
93
94    /// 为表添加租户隔离规则
95    pub fn tenant_isolation(mut self, table: &str, tenant_column: &str) -> Self {
96        if let Some(ref tenant_id) = self.context.tenant_id {
97            self.context.add_rule(AccessRule {
98                table: table.to_string(),
99                row_filter: Some(format!("{} = '{}'", tenant_column, tenant_id)),
100                allowed_columns: None,
101                denied_columns: HashSet::new(),
102            });
103        }
104        self
105    }
106
107    /// 为表添加用户隔离规则
108    pub fn user_isolation(mut self, table: &str, user_column: &str) -> Self {
109        if let Some(ref user_id) = self.context.user_id {
110            self.context.add_rule(AccessRule {
111                table: table.to_string(),
112                row_filter: Some(format!("{} = '{}'", user_column, user_id)),
113                allowed_columns: None,
114                denied_columns: HashSet::new(),
115            });
116        }
117        self
118    }
119
120    /// 禁止查询敏感字段
121    pub fn deny_columns(mut self, table: &str, columns: &[&str]) -> Self {
122        let rule = self
123            .context
124            .rules
125            .entry(table.to_string())
126            .or_insert(AccessRule {
127                table: table.to_string(),
128                row_filter: None,
129                allowed_columns: None,
130                denied_columns: HashSet::new(),
131            });
132        for col in columns {
133            rule.denied_columns.insert(col.to_string());
134        }
135        self
136    }
137
138    pub fn build(self) -> AccessContext {
139        self.context
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_access_context_default() {
149        let ctx = AccessContext::new();
150        assert!(ctx.tenant_id.is_none());
151        assert!(ctx.user_id.is_none());
152        assert!(ctx.roles.is_empty());
153    }
154
155    #[test]
156    fn test_with_tenant_and_user() {
157        let ctx = AccessContext::new()
158            .with_tenant("tenant-1")
159            .with_user("user-1");
160        assert_eq!(ctx.tenant_id.as_deref(), Some("tenant-1"));
161        assert_eq!(ctx.user_id.as_deref(), Some("user-1"));
162    }
163
164    #[test]
165    fn test_column_allowed_by_default() {
166        let ctx = AccessContext::new();
167        assert!(ctx.is_column_allowed("users", "id"));
168        assert!(ctx.is_column_allowed("users", "password"));
169    }
170
171    #[test]
172    fn test_deny_columns() {
173        let mut ctx = AccessContext::new();
174        ctx.add_rule(AccessRule {
175            table: "users".to_string(),
176            row_filter: None,
177            allowed_columns: None,
178            denied_columns: ["password".to_string()].into_iter().collect(),
179        });
180        assert!(!ctx.is_column_allowed("users", "password"));
181        assert!(ctx.is_column_allowed("users", "id"));
182    }
183
184    #[test]
185    fn test_allowed_columns_whitelist() {
186        let mut ctx = AccessContext::new();
187        let mut allowed: HashSet<String> = HashSet::new();
188        allowed.insert("id".to_string());
189        allowed.insert("name".to_string());
190        ctx.add_rule(AccessRule {
191            table: "users".to_string(),
192            row_filter: None,
193            allowed_columns: Some(allowed),
194            denied_columns: HashSet::new(),
195        });
196        assert!(ctx.is_column_allowed("users", "id"));
197        assert!(ctx.is_column_allowed("users", "name"));
198        assert!(!ctx.is_column_allowed("users", "secret"));
199    }
200
201    #[test]
202    fn test_filter_columns() {
203        let mut ctx = AccessContext::new();
204        ctx.add_rule(AccessRule {
205            table: "users".to_string(),
206            row_filter: None,
207            allowed_columns: None,
208            denied_columns: ["password".to_string()].into_iter().collect(),
209        });
210        let cols = vec!["id".to_string(), "name".to_string(), "password".to_string()];
211        let filtered = ctx.filter_columns("users", &cols);
212        assert_eq!(filtered, vec!["id".to_string(), "name".to_string()]);
213    }
214
215    #[test]
216    fn test_row_filter() {
217        let mut ctx = AccessContext::new();
218        ctx.add_rule(AccessRule {
219            table: "orders".to_string(),
220            row_filter: Some("tenant_id = 't1'".to_string()),
221            allowed_columns: None,
222            denied_columns: HashSet::new(),
223        });
224        assert_eq!(ctx.row_filter("orders"), Some("tenant_id = 't1'"));
225        assert_eq!(ctx.row_filter("users"), None);
226    }
227
228    #[test]
229    fn test_row_level_security_tenant_isolation() {
230        let ctx = AccessContext::new().with_tenant("tenant-42");
231        let built = RowLevelSecurity::new(ctx)
232            .tenant_isolation("orders", "tenant_id")
233            .build();
234        assert_eq!(built.row_filter("orders"), Some("tenant_id = 'tenant-42'"));
235    }
236
237    #[test]
238    fn test_row_level_security_user_isolation() {
239        let ctx = AccessContext::new().with_user("u-1");
240        let built = RowLevelSecurity::new(ctx)
241            .user_isolation("profiles", "user_id")
242            .build();
243        assert_eq!(built.row_filter("profiles"), Some("user_id = 'u-1'"));
244    }
245
246    #[test]
247    fn test_row_level_security_deny_columns() {
248        let ctx = AccessContext::new();
249        let built = RowLevelSecurity::new(ctx)
250            .deny_columns("users", &["password", "salt"])
251            .build();
252        assert!(!built.is_column_allowed("users", "password"));
253        assert!(!built.is_column_allowed("users", "salt"));
254        assert!(built.is_column_allowed("users", "id"));
255    }
256
257    #[test]
258    fn test_tenant_isolation_skipped_without_tenant() {
259        // 未设置 tenant_id 时不应添加规则
260        let ctx = AccessContext::new();
261        let built = RowLevelSecurity::new(ctx)
262            .tenant_isolation("orders", "tenant_id")
263            .build();
264        assert_eq!(built.row_filter("orders"), None);
265    }
266}