Skip to main content

rust_store_core/command/
write.rs

1//! 写路径与简单查询计划
2
3use serde_json::{json, Map, Value};
4
5use crate::computes::{apply_defaults_and_computes, FnRegistry};
6use crate::permission::{can_write_schema, evaluate, merge_owner_condition, Context, Doc};
7use crate::rbac::{
8    ensure_write, ensure_write_on_doc, filter_writable_data_overlay, merge_row_condition,
9    probe_condition_fields, WriteAction,
10};
11use crate::schema::{Registry, Schema};
12use crate::types::{
13    has_relation_predicate, is_truthy, validate_condition, validate_condition_shape,
14};
15
16use super::cmd::{cmd_count_documents, cmd_find_one, cmd_insert_one};
17use super::write_links::attach_write_links;
18use super::{ensure_context, ERR_NO_WRITE};
19
20/// 生成插入命令(对应 JS `insert`)
21///
22/// `now` 与 `new_id` 由 Host 提供(core 无时钟与随机源,且这样可让产出确定、便于对拍)。
23pub fn plan_insert(
24    schema_name: &str,
25    registry: &Registry,
26    ctx: Option<&Context>,
27    data: &Value,
28    now: i64,
29    new_id: &str,
30    fn_registry: Option<&dyn FnRegistry>,
31) -> Result<Value, String> {
32    ensure_context(registry, ctx)?;
33    let schema = registry.get(schema_name)?;
34
35    if !can_write_schema(registry.role_rules(), schema, ctx) {
36        return Err(ERR_NO_WRITE.to_string());
37    }
38    // RBAC 表级写判定(deny-wins,叠加于静态白名单之后)
39    ensure_write(registry, schema, ctx, WriteAction::Insert)?;
40
41    let doc = build_insert_doc(registry, schema, ctx, data, now, new_id)?;
42    let doc = Value::Object(doc);
43    // 触发链展开(insert 事件;text2query 档下含触发器即 Err —— A9)
44    let triggers = super::triggers::expand_triggers(registry, ctx, schema_name, "insert")?;
45    let mut plan = json!({
46        "command": cmd_insert_one(schema, &doc),
47        "returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
48    });
49    // 未配置触发器时不出现 triggers 键(A2 零回归)
50    if !triggers.is_empty() {
51        plan.as_object_mut()
52            .expect("plan_insert:plan 必为对象")
53            .insert("triggers".to_string(), json!(triggers));
54    }
55    // 写链路附接(多落点 schema 才追加 writeLinks;单落点原样返回)
56    attach_write_links(plan, registry, &[schema_name], registry.write_link_policy())
57}
58
59/// insert 文档规范化(对应 JS `insert` 主体):
60/// 权限过滤 → 保留显式 null → 自动 _id / createdBy / 时间戳
61pub(super) fn build_insert_doc(
62    registry: &Registry,
63    schema: &Schema,
64    ctx: Option<&Context>,
65    data: &Value,
66    now: i64,
67    new_id: &str,
68) -> Result<Map<String, Value>, String> {
69    let filtered = match ctx {
70        // RBAC 感知版:静态 writable ∩ RBAC writeFields(无策略时直通静态过滤)
71        Some(_) => filter_writable_data_overlay(registry, schema, ctx, data),
72        None => data.clone(),
73    };
74    // accessKeys:字段白名单过滤后校验目标值(被过滤掉的字段不落库,无需校验)
75    if let Some(obj) = filtered.as_object() {
76        crate::access::validate_write_values(schema, ctx, obj)?;
77    }
78
79    // 保留显式 null:历史实现把 null 一律剔除(当成缺失),导致「空串/NULL/缺失」三态
80    // 无法区分(F-07),null 字段读回也被吞(H-01)。显式 `null` 应原样落库,SQL 落 NULL。
81    let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
82
83    // 自动生成 ID(仅在 schema 配了 idPrefix 且未提供有效 _id 时);
84    // 无 idPrefix 且无 _id → 显式报错(缺陷 D-03):静默产出「无 _id 文档」会让
85    // Host 返回 _id=undefined,且 Mongo 自动 ObjectId 会触发跨绑定序列化崩溃
86    if !doc.get("_id").map(is_truthy).unwrap_or(false) {
87        if schema.id_is_autoincrement() {
88            // 阶段2:数据库自增主键 —— doc 不注入 `_id`(SQL INSERT 列白名单来自 doc 键,
89            // 无 `_id` 列即由数据库赋值;PG/SQLite 走 RETURNING 回读、MySQL 走宿主
90            // lastrowid)。Mongo 无自增语义,由宿主 Mongo 执行器对此形态显式报错
91            // (AUTOINCREMENT_NOT_SUPPORTED),禁 ObjectId 静默顶替。
92        } else if !schema.id_prefix.is_empty() {
93            doc.insert("_id".to_string(), json!(new_id));
94        } else {
95            return Err(format!(
96                "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
97                schema.name
98            ));
99        }
100    }
101
102    // 自动设置 createdBy(creator 权限场景)
103    if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
104        if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
105            doc.insert("createdBy".to_string(), json!(uid));
106        }
107    }
108
109    // 自动时间戳
110    if schema.timestamps {
111        if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
112            doc.insert("createdAt".to_string(), json!(now));
113        }
114        doc.insert("updatedAt".to_string(), json!(now));
115    }
116    Ok(doc)
117}
118
119/// 判断存在性(对应 JS `exists`)
120pub fn plan_exists(
121    schema_name: &str,
122    registry: &Registry,
123    condition: &Value,
124) -> Result<Value, String> {
125    // 条件拒绝名单(缺陷 D-02)
126    validate_condition(condition)?;
127    let schema = registry.get(schema_name)?;
128    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
129    validate_condition_shape(schema, condition, registry.profile())?;
130    // §9.6 关系聚合谓词无法用标量 `findOne` 表达 → 显式 Err(否则 Mongo 静默给错结果)
131    if has_relation_predicate(schema, condition) {
132        return Err(
133            "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
134             请改用 query 并自行判断是否有结果"
135                .to_string(),
136        );
137    }
138    Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
139}
140
141/// 统计数量(对应 JS `count`):filter 为 nullish 时用 `{}`
142///
143/// R1:owner(read=creator) 场景下,count 必须注入 `createdBy = ctx.userId`,
144/// 否则非 admin 用户 count 会越权统计全表(E-08)。
145pub fn plan_count(
146    schema_name: &str,
147    registry: &Registry,
148    filter: Option<&Value>,
149    ctx: Option<&Context>,
150) -> Result<Value, String> {
151    if let Some(f) = filter {
152        // 条件拒绝名单(缺陷 D-02)
153        validate_condition(f)?;
154    }
155    let schema = registry.get(schema_name)?;
156    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
157    if let Some(f) = filter {
158        validate_condition_shape(schema, f, registry.profile())?;
159        // §9.6 关系聚合谓词无法用标量 count 表达 → 显式 Err(与 query_with_count 同口径)
160        if has_relation_predicate(schema, f) {
161            return Err(
162                "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
163                 请改用 query 并在调用方自行统计"
164                    .to_string(),
165            );
166        }
167    }
168    let base = match filter {
169        None | Some(Value::Null) => json!({}),
170        Some(v) => v.clone(),
171    };
172    // 静态 owner 条件叠加 RBAC 行条件(两引擎 $and,deny-wins)
173    let owner_merged = merge_owner_condition(registry.role_rules(), schema, ctx, Some(base));
174    let filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)?
175        .unwrap_or_else(|| json!({}));
176    Ok(cmd_count_documents(schema, &filter))
177}
178
179pub(super) fn has_creator_permission(schema: &Schema) -> bool {
180    let has = |list: &Option<Vec<String>>| {
181        list.as_ref()
182            .map(|l| l.iter().any(|r| r == "creator"))
183            .unwrap_or(false)
184    };
185    has(&schema.read) || has(&schema.write)
186}
187
188// ─── 写权限探针(对应 JS `_checkWritePerm`) ─────────────────
189
190/// creator 写权限的探针状态:
191/// `NotProbed` → 尚未探查(可能返回需要探针的命令)
192/// `NoResult` → 探针无结果(拒绝)
193/// `Found`    → 探针命中的 `{_id, createdBy}` 文档
194#[derive(Debug, Clone, Copy)]
195pub enum Probe<'a> {
196    NotProbed,
197    NoResult,
198    Found(&'a Value),
199}
200
201/// Schema 级写权限检查(对应 JS `_checkWritePerm`):
202/// 拒写清单命中直接拒绝;非写授权时仅 creator 命中才放行(需 Host 先执行探针命令)。
203///
204/// RBAC 叠加(deny-wins):
205/// - 表级:`ensure_write(action)` 在 guest 检查后判定,deny 即拒;
206/// - 行级:`row_condition` 非空(ownerOnly / condition)时**同样需要探针**——
207///   静态放行的路径也要先取 `{_id, 归属键, condition 键}` 再重入判定
208///   ([`ensure_write_on_doc`]),杜绝 ownerOnly 单条更新静默命中他人文档;
209/// - 探针重入(`Probe::Found`)时静态 `evaluate` 与 RBAC 行级判定都过才放行。
210///
211/// `extra_probe_fields`:触发器探针需额外投影的字段(`onFields` ∪ `{{before.*}}`
212/// 引用,见 `command::triggers::before_probe_fields`)。非空时即使权限本不需探针
213/// 也发探针(Host 据探针结果替换 `{{before.*}}` 并判定字段级命中);为空时行为
214/// 与无参版本**逐字节等价**(零回归契约)。
215///
216/// 返回:`Ok(None)` = 放行;`Ok(Some(cmd))` = Host 先执行该 findOne 探针后携
217/// [`Probe::Found`] / [`Probe::NoResult`] 重入;`Err` = 拒绝(Host 映射 PermissionError)。
218#[allow(clippy::too_many_arguments)]
219pub fn check_write_perm(
220    registry: &Registry,
221    schema: &Schema,
222    ctx: Option<&Context>,
223    condition: &Value,
224    deny_msg: &str,
225    probe: Probe,
226    action: WriteAction,
227    extra_probe_fields: &[String],
228) -> Result<Option<Value>, String> {
229    let probe_projection = |extra: &[String]| -> Value {
230        let mut m = Map::new();
231        m.insert("_id".to_string(), json!(1));
232        // 探针必须含归属键(schema.owner_key(),缺省 createdBy):creator 判定 /
233        // ownerOnly 行级检查都从探针文档取归属键,ownerKey='_id' 等自定义键同样命中
234        m.insert(schema.owner_key().to_string(), json!(1));
235        for f in extra {
236            // 仅取 schema 已声明字段(防触发器声明引用未声明键污染投影)
237            if schema.fields.contains_key(f) {
238                m.insert(f.clone(), json!(1));
239            }
240        }
241        Value::Object(m)
242    };
243
244    // 无 ctx(内部调用):权限不设防,但触发器探针(`before` 值供给 / onFields 命中
245    // 判定)**与权限无关**,必须发 —— 否则字段级判定退化为「全命中」,静默放大触发面
246    // (no-op 抑制失效)。投影只含 `_id` + 触发器引用字段(不带 createdBy —— 触发器探针
247    // 无 creator 判定语义,且目标 schema 未声明 createdBy 时 SQL 列不存在)。
248    // 有 ctx 时走下方权限路径:权限探针投影已并入 extra(onFields ∪ before 引用)。
249    if ctx.is_none() {
250        if !extra_probe_fields.is_empty() {
251            if let Probe::NotProbed = probe {
252                let mut m = Map::new();
253                m.insert("_id".to_string(), json!(1));
254                for f in extra_probe_fields {
255                    // 仅取 schema 已声明字段(防触发器声明引用未声明键污染投影)
256                    if schema.fields.contains_key(f) {
257                        m.insert(f.clone(), json!(1));
258                    }
259                }
260                return Ok(Some(cmd_find_one(
261                    schema,
262                    condition,
263                    Some(&Value::Object(m)),
264                )));
265            }
266        }
267        return Ok(None);
268    }
269    let Some(c) = ctx else {
270        unreachable!("上方已处理 ctx.is_none()");
271    };
272    // 拒写清单(默认空——无拒写;原 guest 硬编码随清单化移除,设计 §11.4)
273    let rules = registry.role_rules();
274    if crate::permission::has_any_role(c, &rules.deny_write_roles) {
275        return Err(deny_msg.to_string());
276    }
277    // RBAC 表级写判定(deny-wins;拒绝文案自带 RBAC 标识,不走入参 deny_msg)
278    ensure_write(registry, schema, Some(c), action)?;
279    // accessKeys 写受限判定(缺清单键 / 非法形态 → Err,fail closed)
280    let access_restricted = crate::access::write_restricted(schema, Some(c))?;
281    // access 受控但条件为空 → 拒绝(否则会无条件下写整表)。
282    // 注意:空对象 `{}` 在 `is_truthy` 下为真,必须显式按「无键」判定为空条件。
283    let has_condition = condition
284        .as_object()
285        .map(|o| !o.is_empty())
286        .unwrap_or_else(|| is_truthy(condition));
287    if access_restricted && !has_condition {
288        return Err(format!(
289            "{}条件为空,拒绝无条件下写入受 accessKeys 管控的模型",
290            crate::access::ERR_ACCESS_PREFIX
291        ));
292    }
293    // RBAC 行级限制是否存在(存在则探针投影需并入 condition 字段键)
294    let rbac_row_restricted =
295        crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
296    let row_restricted = rbac_row_restricted || access_restricted;
297
298    // 探针投影字段全集:RBAC 行级条件字段 ∪ 触发器 onFields / before 引用字段
299    // ∪ accessKeys 受控字段(探针文档需按子集判定当前值)
300    let mut merged = probe_condition_fields(registry, schema, Some(c), action.as_str());
301    for f in crate::access::probe_fields(schema, Some(c))? {
302        if !merged.contains(&f) {
303            merged.push(f);
304        }
305    }
306    for f in extra_probe_fields {
307        if !merged.contains(f) {
308            merged.push(f.clone());
309        }
310    }
311
312    if can_write_schema(registry.role_rules(), schema, Some(c)) {
313        // 静态放行:RBAC 行级 / accessKeys 受控时仍需探针(否则静默放行越权行更新/删除)
314        if row_restricted && is_truthy(condition) {
315            return match probe {
316                Probe::NotProbed => Ok(Some(cmd_find_one(
317                    schema,
318                    condition,
319                    Some(&probe_projection(&merged)),
320                ))),
321                Probe::NoResult => Err(deny_msg.to_string()),
322                Probe::Found(doc) => {
323                    if access_restricted {
324                        crate::access::validate_doc_values(schema, Some(c), doc)?;
325                    }
326                    if rbac_row_restricted {
327                        ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
328                    } else {
329                        Ok(None)
330                    }
331                }
332            };
333        }
334        // 触发器需要 before 值 / 字段级命中判定 → 权限本不需探针也发探针
335        // (静态权限已由 can_write_schema 放行;Found 重入直接放行)
336        if !extra_probe_fields.is_empty() {
337            return match probe {
338                Probe::NotProbed => Ok(Some(cmd_find_one(
339                    schema,
340                    condition,
341                    Some(&probe_projection(&merged)),
342                ))),
343                Probe::NoResult => Err(deny_msg.to_string()),
344                Probe::Found(_) => Ok(None),
345            };
346        }
347        return Ok(None);
348    }
349    let creator_only = schema
350        .write
351        .as_ref()
352        .map(|w| w.iter().any(|r| r == "creator"))
353        .unwrap_or(false);
354    if creator_only && is_truthy(condition) {
355        return match probe {
356            Probe::NotProbed => Ok(Some(cmd_find_one(
357                schema,
358                condition,
359                Some(&probe_projection(&merged)),
360            ))),
361            Probe::NoResult => Err(deny_msg.to_string()),
362            Probe::Found(doc) => {
363                if evaluate(
364                    registry.role_rules(),
365                    Some(c),
366                    schema.write.as_deref(),
367                    Doc::Doc(doc),
368                    schema.owner_key(),
369                ) {
370                    // accessKeys 子集判定先于 RBAC 行级判定(两者都过才放行)
371                    if access_restricted {
372                        crate::access::validate_doc_values(schema, Some(c), doc)?;
373                    }
374                    if rbac_row_restricted {
375                        // 静态 creator 过 → RBAC 行级再判(两引擎都过才放行)
376                        ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
377                    } else {
378                        Ok(None)
379                    }
380                } else {
381                    Err(deny_msg.to_string())
382                }
383            }
384        };
385    }
386    Err(deny_msg.to_string())
387}