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 键, 清单)`;缺键 / 非法形态 → `Err`。
117fn active_write_keys<'a>(
118    schema: &'a Schema,
119    c: &'a Context,
120) -> Result<Vec<(&'a AccessKeyDef, &'a str, &'a Vec<Value>)>, String> {
121    let mut out = Vec::new();
122    for def in &schema.access_keys {
123        if is_bypassed(def, c) {
124            continue;
125        }
126        let Some(key) = def.write.as_deref() else {
127            continue;
128        };
129        let list = resolve_list(c, key, &def.field)?;
130        out.push((def, key, list));
131    }
132    Ok(out)
133}
134
135fn is_array_field(schema: &Schema, field: &str) -> bool {
136    schema
137        .fields
138        .get(field)
139        .map(|f| f.field_type == "array")
140        .unwrap_or(false)
141}
142
143fn empty_list_err(key: &str, field: &str) -> String {
144    format!("{ERR_ACCESS_PREFIX}清单 \"{key}\" 为空,拒绝写入字段 \"{field}\"")
145}
146
147/// payload 目标值校验:单值目标 ∈ 清单;`array` 目标 ⊆ 清单。
148///
149/// 仅校验 `data` 顶层出现的受控字段;点号路径改写(`field.xxx`)一律拒绝,
150/// 防止绕过值校验。
151pub fn validate_write_values(
152    schema: &Schema,
153    ctx: Option<&Context>,
154    data: &Map<String, Value>,
155) -> Result<(), String> {
156    let Some(c) = ctx.filter(|c| !c.internal) else {
157        return Ok(());
158    };
159    for (def, key, list) in active_write_keys(schema, c)? {
160        let field = def.field.as_str();
161        if let Some(path) = data.keys().find(|k| k.starts_with(&format!("{field}."))) {
162            return Err(format!(
163                "{ERR_ACCESS_PREFIX}不允许通过点号路径 \"{path}\" 改写受 accessKeys 管控的字段 \"{field}\""
164            ));
165        }
166        let Some(target) = data.get(field) else {
167            continue;
168        };
169        if list.is_empty() {
170            return Err(empty_list_err(key, field));
171        }
172        if is_array_field(schema, field) {
173            let Some(items) = target.as_array() else {
174                return Err(format!(
175                    "{ERR_ACCESS_PREFIX}字段 \"{field}\" 为数组,目标值必须为数组(实际 {target})"
176                ));
177            };
178            for item in items {
179                if !list.contains(item) {
180                    return Err(format!(
181                        "{ERR_ACCESS_PREFIX}字段 \"{field}\" 的目标数组含清单 \"{key}\" 之外的元素 {item}"
182                    ));
183                }
184            }
185        } else if !list.contains(target) {
186            return Err(format!(
187                "{ERR_ACCESS_PREFIX}字段 \"{field}\" 的目标值 {target} 不在清单 \"{key}\" 中"
188            ));
189        }
190    }
191    Ok(())
192}
193
194/// 探针文档校验:对**被操作文档当前值**做单值 ∈ 清单 / `array` ⊆ 清单(子集,非交集)。
195pub fn validate_doc_values(
196    schema: &Schema,
197    ctx: Option<&Context>,
198    doc: &Value,
199) -> Result<(), String> {
200    let Some(c) = ctx.filter(|c| !c.internal) else {
201        return Ok(());
202    };
203    for (def, key, list) in active_write_keys(schema, c)? {
204        let field = def.field.as_str();
205        if list.is_empty() {
206            return Err(empty_list_err(key, field));
207        }
208        if is_array_field(schema, field) {
209            match doc.get(field) {
210                None | Some(Value::Null) => {}
211                Some(Value::Array(items)) => {
212                    for item in items {
213                        if !list.contains(item) {
214                            return Err(format!(
215                                "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 含清单 \"{key}\" 之外的元素 {item}"
216                            ));
217                        }
218                    }
219                }
220                Some(other) => {
221                    return Err(format!(
222                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 非数组,无法做子集判定(实际 {other})"
223                    ))
224                }
225            }
226        } else {
227            match doc.get(field) {
228                None | Some(Value::Null) => {
229                    return Err(format!(
230                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 缺失,不在清单 \"{key}\" 中"
231                    ))
232                }
233                Some(v) if !list.contains(v) => {
234                    return Err(format!(
235                        "{ERR_ACCESS_PREFIX}文档字段 \"{field}\" 的值 {v} 不在清单 \"{key}\" 中"
236                    ))
237                }
238                Some(_) => {}
239            }
240        }
241    }
242    Ok(())
243}
244
245/// 原生操作符(`$inc` / `$unset` / `$push` / `$pull` / `$addToSet` 等)不得改写
246/// 受控字段;`$set` 放行(其值已由 [`validate_write_values`] 校验)。
247pub fn validate_raw_operators(
248    schema: &Schema,
249    ctx: Option<&Context>,
250    update: &Value,
251) -> Result<(), String> {
252    let Some(c) = ctx.filter(|c| !c.internal) else {
253        return Ok(());
254    };
255    let pairs = active_write_keys(schema, c)?;
256    if pairs.is_empty() {
257        return Ok(());
258    }
259    let Some(obj) = update.as_object() else {
260        return Ok(());
261    };
262    for (op, val) in obj {
263        if !op.starts_with('$') || op == "$set" {
264            continue;
265        }
266        let Some(fields) = val.as_object() else {
267            continue;
268        };
269        for name in fields.keys() {
270            let root = name.split('.').next().unwrap_or(name);
271            if let Some((def, key, _)) = pairs.iter().find(|(d, _, _)| d.field == root) {
272                if def.write.as_deref() == Some(*key) {
273                    return Err(format!(
274                        "{ERR_ACCESS_PREFIX}操作符 {op} 不允许改写受 accessKeys 管控的字段 \"{root}\""
275                    ));
276                }
277            }
278        }
279    }
280    Ok(())
281}
282
283/// 探针需额外投影的受控字段名(缺键即 `Err`,保证探针前已完成 fail closed)。
284pub fn probe_fields(schema: &Schema, ctx: Option<&Context>) -> Result<Vec<String>, String> {
285    let Some(c) = ctx.filter(|c| !c.internal) else {
286        return Ok(Vec::new());
287    };
288    Ok(active_write_keys(schema, c)?
289        .into_iter()
290        .map(|(def, _, _)| def.field.clone())
291        .collect())
292}