Skip to main content

rust_store_core/
access.rs

1//! `accessKeys` 运行时判决(读路径行条件 + 写路径子集校验的公共解析)。
2//!
3//! 声明解析见 [`crate::schema::access_keys`];本模块只消费已校验的
4//! [`crate::schema::AccessKeyDef`] 与 [`crate::permission::Context`] 清单。
5//!
6//! 语义:
7//! - 读:`{字段: {"$in": 清单}}`,与 owner / RBAC 条件 `$and` 叠加;
8//!   单值字段 = 标量成员判定,`array` 字段 = 交集命中(Mongo 原生 `$in` 语义);
9//! - 写:单值目标 ∈ 清单、`array` 目标 ⊆ 清单(见写校验函数);
10//! - ctx 缺键且未 bypass → `ERR_PERMISSION:ACCESS:`(fail closed,禁静默放行);
11//! - `清单: []` 是结论(读 0 行 / 写全拒),缺键是错误,两者严格区分。
12
13use serde_json::{json, Map, Value};
14
15use crate::permission::Context;
16use crate::schema::{AccessKeyDef, Schema};
17
18/// accessKeys 拒绝错误统一前缀(Host 依此前缀映射 PermissionError)
19pub const ERR_ACCESS_PREFIX: &str = "ERR_PERMISSION:ACCESS:";
20
21/// 角色是否命中该键的 `bypassRoles`(`internal` 恒豁免)。
22pub fn is_bypassed(def: &AccessKeyDef, ctx: &Context) -> bool {
23    if ctx.internal {
24        return true;
25    }
26    let mut roles: Vec<&str> = Vec::new();
27    if let Some(rs) = &ctx.roles {
28        roles.extend(rs.iter().map(String::as_str));
29    }
30    if let Some(r) = &ctx.role {
31        roles.push(r.as_str());
32    }
33    roles
34        .iter()
35        .any(|r| def.bypass_roles.iter().any(|b| b == r))
36}
37
38/// 取 ctx 清单;缺键 / 形态非法 → 显式 Err(fail closed)。
39pub fn resolve_list<'a>(
40    ctx: &'a Context,
41    ctx_key: &str,
42    field: &str,
43) -> Result<&'a Vec<Value>, String> {
44    if let Some(err) = ctx.access_list_errors.get(ctx_key) {
45        return Err(format!(
46            "{ERR_ACCESS_PREFIX}ctx 键 \"{ctx_key}\" 的{err}(字段 \"{field}\" 的行级权限清单非法)"
47        ));
48    }
49    match ctx.access_lists.get(ctx_key) {
50        Some(Value::Array(items)) => Ok(items),
51        Some(_) => Err(format!(
52            "{ERR_ACCESS_PREFIX}ctx 键 \"{ctx_key}\" 的清单必须为数组"
53        )),
54        None => Err(format!(
55            "{ERR_ACCESS_PREFIX}ctx 缺少字段 \"{field}\" 的行级权限清单键 \"{ctx_key}\"\
56             (角色未命中 bypassRoles,拒绝静默放行)"
57        )),
58    }
59}
60
61/// 读/写行条件:按 action 取 `read` / `write` 引用键,逐键生成 `$in` 条件并 `$and`。
62///
63/// - `ctx: None` → `None`(沿用「无 ctx = 系统内部调用跳过权限」契约);
64/// - `read` 用于查询;`update` / `remove` 用于批量写行收紧(使用 `write` 键);
65/// - `write` 缺省 = 该键只读控,写路径不生成条件。
66pub fn row_condition(
67    schema: &Schema,
68    ctx: Option<&Context>,
69    action: &str,
70) -> Result<Option<Value>, String> {
71    let Some(c) = ctx else {
72        return Ok(None);
73    };
74    if !matches!(action, "read" | "update" | "remove") {
75        return Ok(None);
76    }
77    let mut conds: Vec<Value> = Vec::new();
78    for def in &schema.access_keys {
79        if is_bypassed(def, c) {
80            continue;
81        }
82        let key = if action == "read" {
83            Some(def.read.as_str())
84        } else {
85            def.write.as_deref()
86        };
87        let Some(key) = key else {
88            continue;
89        };
90        let list = resolve_list(c, key, &def.field)?;
91        let mut obj = serde_json::Map::new();
92        obj.insert(def.field.clone(), json!({ "$in": list.clone() }));
93        conds.push(Value::Object(obj));
94    }
95    Ok(match conds.len() {
96        0 => None,
97        1 => Some(conds.remove(0)),
98        _ => Some(json!({ "$and": conds })),
99    })
100}
101
102// ─── 写路径 ──────────────────────────────────────────────────
103
104/// 该 schema 本次写是否受 accessKeys 管控(`ctx=None` / `internal` / 全部 bypass /
105/// `write=None` → `false`)。
106///
107/// 缺清单键 / 清单非数组 → 显式 `Err`(fail closed);`清单: []` 亦返回 `true`
108/// (写全拒由各 `validate_*` 给出「清单为空」错误)。
109pub fn write_restricted(schema: &Schema, ctx: Option<&Context>) -> Result<bool, String> {
110    let Some(c) = ctx.filter(|c| !c.internal) else {
111        return Ok(false);
112    };
113    Ok(!active_write_keys(schema, c)?.is_empty())
114}
115
116/// 单个受控写键:`(定义, ctx 键, 清单)`。
117type ActiveWriteKey<'a> = (&'a AccessKeyDef, &'a str, &'a Vec<Value>);
118
119/// 收集本次写实际受控的 `(定义, ctx 键, 清单)`;缺键 / 非法形态 → `Err`。
120fn active_write_keys<'a>(
121    schema: &'a Schema,
122    c: &'a Context,
123) -> Result<Vec<ActiveWriteKey<'a>>, String> {
124    let mut out = Vec::new();
125    for def in &schema.access_keys {
126        if is_bypassed(def, c) {
127            continue;
128        }
129        let Some(key) = def.write.as_deref() else {
130            continue;
131        };
132        let list = resolve_list(c, key, &def.field)?;
133        out.push((def, key, list));
134    }
135    Ok(out)
136}
137
138fn is_array_field(schema: &Schema, field: &str) -> bool {
139    schema
140        .fields
141        .get(field)
142        .map(|f| f.field_type == "array")
143        .unwrap_or(false)
144}
145
146fn empty_list_err(key: &str, field: &str) -> String {
147    format!("{ERR_ACCESS_PREFIX}清单 \"{key}\" 为空,拒绝写入字段 \"{field}\"")
148}
149
150/// payload 目标值校验:单值目标 ∈ 清单;`array` 目标 ⊆ 清单。
151///
152/// 仅校验 `data` 顶层出现的受控字段;点号路径改写(`field.xxx`)一律拒绝,
153/// 防止绕过值校验。
154pub fn validate_write_values(
155    schema: &Schema,
156    ctx: Option<&Context>,
157    data: &Map<String, Value>,
158) -> Result<(), String> {
159    let Some(c) = ctx.filter(|c| !c.internal) else {
160        return Ok(());
161    };
162    for (def, key, list) in active_write_keys(schema, c)? {
163        let field = def.field.as_str();
164        if let Some(path) = data.keys().find(|k| k.starts_with(&format!("{field}."))) {
165            return Err(format!(
166                "{ERR_ACCESS_PREFIX}不允许通过点号路径 \"{path}\" 改写受 accessKeys 管控的字段 \"{field}\""
167            ));
168        }
169        let Some(target) = data.get(field) else {
170            continue;
171        };
172        if list.is_empty() {
173            return Err(empty_list_err(key, field));
174        }
175        if is_array_field(schema, field) {
176            let Some(items) = target.as_array() else {
177                return Err(format!(
178                    "{ERR_ACCESS_PREFIX}字段 \"{field}\" 为数组,目标值必须为数组(实际 {target})"
179                ));
180            };
181            for item in items {
182                if !list.contains(item) {
183                    return Err(format!(
184                        "{ERR_ACCESS_PREFIX}字段 \"{field}\" 的目标数组含清单 \"{key}\" 之外的元素 {item}"
185                    ));
186                }
187            }
188        } else if !list.contains(target) {
189            return Err(format!(
190                "{ERR_ACCESS_PREFIX}字段 \"{field}\" 的目标值 {target} 不在清单 \"{key}\" 中"
191            ));
192        }
193    }
194    Ok(())
195}
196
197/// 探针文档校验:对**被操作文档当前值**做单值 ∈ 清单 / `array` ⊆ 清单(子集,非交集)。
198pub fn validate_doc_values(
199    schema: &Schema,
200    ctx: Option<&Context>,
201    doc: &Value,
202) -> Result<(), String> {
203    let Some(c) = ctx.filter(|c| !c.internal) else {
204        return Ok(());
205    };
206    for (def, key, list) in active_write_keys(schema, c)? {
207        let field = def.field.as_str();
208        if list.is_empty() {
209            return Err(empty_list_err(key, field));
210        }
211        if is_array_field(schema, field) {
212            match doc.get(field) {
213                None | Some(Value::Null) => {}
214                Some(Value::Array(items)) => {
215                    for item in items {
216                        if !list.contains(item) {
217                            return Err(format!(
218                                "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 含清单 \"{key}\" 之外的元素 {item}"
219                            ));
220                        }
221                    }
222                }
223                Some(other) => {
224                    return Err(format!(
225                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 非数组,无法做子集判定(实际 {other})"
226                    ))
227                }
228            }
229        } else {
230            match doc.get(field) {
231                None | Some(Value::Null) => {
232                    return Err(format!(
233                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 缺失,不在清单 \"{key}\" 中"
234                    ))
235                }
236                Some(v) if !list.contains(v) => {
237                    return Err(format!(
238                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 的值 {v} 不在清单 \"{key}\" 中"
239                    ))
240                }
241                Some(_) => {}
242            }
243        }
244    }
245    Ok(())
246}
247
248/// 原生操作符(`$inc` / `$unset` / `$push` / `$pull` / `$addToSet` 等)不得改写
249/// 受控字段;`$set` 放行(其值已由 [`validate_write_values`] 校验)。
250pub fn validate_raw_operators(
251    schema: &Schema,
252    ctx: Option<&Context>,
253    update: &Value,
254) -> Result<(), String> {
255    let Some(c) = ctx.filter(|c| !c.internal) else {
256        return Ok(());
257    };
258    let pairs = active_write_keys(schema, c)?;
259    if pairs.is_empty() {
260        return Ok(());
261    }
262    let Some(obj) = update.as_object() else {
263        return Ok(());
264    };
265    for (op, val) in obj {
266        if !op.starts_with('$') || op == "$set" {
267            continue;
268        }
269        let Some(fields) = val.as_object() else {
270            continue;
271        };
272        for name in fields.keys() {
273            let root = name.split('.').next().unwrap_or(name);
274            if let Some((def, key, _)) = pairs.iter().find(|(d, _, _)| d.field == root) {
275                if def.write.as_deref() == Some(*key) {
276                    return Err(format!(
277                        "{ERR_ACCESS_PREFIX}操作符 {op} 不允许改写受 accessKeys 管控的字段 \"{root}\""
278                    ));
279                }
280            }
281        }
282    }
283    Ok(())
284}
285
286/// 探针需额外投影的受控字段名(缺键即 `Err`,保证探针前已完成 fail closed)。
287pub fn probe_fields(schema: &Schema, ctx: Option<&Context>) -> Result<Vec<String>, String> {
288    let Some(c) = ctx.filter(|c| !c.internal) else {
289        return Ok(Vec::new());
290    };
291    Ok(active_write_keys(schema, c)?
292        .into_iter()
293        .map(|(def, _, _)| def.field.clone())
294        .collect())
295}