Skip to main content

rust_store_core/
rbac.rs

1//! RBAC 动态策略引擎(core 层判决插件;静态白名单引擎见 [`crate::permission`])
2//!
3//! 与静态引擎的叠加语义(deny-wins):RBAC 判决叠加在静态判决**之后**,任一拒绝即
4//! 拒绝;字段维度取交集;行条件与静态 owner 条件 `$and`。策略未注入(`registry.rbac()`
5//! 为 `None`)时全部原语直通,行为与现状逐位一致——增量收紧,永不放大既有权限面。
6//!
7//! 两个容易搞反的维度:
8//! - grant 之间(同一用户多角色 / 多 grant):**OR**(任一 grant 授予即放行其
9//!   条件命中的行 / 并集字段)——授权是并集;
10//! - 引擎之间(静态白名单 × RBAC):**AND**(deny-wins)——收紧取交集。
11//!
12//! 行为契约(与 [`crate::permission`] 一致,勿破坏):
13//! - `ctx: None` / `internal` / 豁免清单(`exempt_roles`,默认空)命中 → RBAC 不介入(直通);
14//! - 拒绝一律携带 `ERR_PERMISSION:` 稳定前缀(宿主按前缀映射 PermissionError),
15//!   二级标识 `RBAC:` 供日志与测试区分。
16
17use std::collections::HashSet;
18
19use serde_json::{json, Map, Value};
20
21use crate::permission::{get_writable_fields, Context};
22use crate::schema::{Registry, Schema};
23use crate::types::is_truthy;
24
25/// 策略模式:
26/// - [`RbacMode::Overlay`](默认):存在匹配 grant 才生效(按 model 增量收紧);
27/// - [`RbacMode::Enforce`]:受管角色(出现在 `roles` 声明中)对**所有** model 生效,
28///   无匹配 grant 即拒绝(default deny);未受管角色不介入。
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum RbacMode {
31    #[default]
32    Overlay,
33    Enforce,
34}
35
36/// 写动作粒度(对齐静态引擎 `check_write_perm` 的 `ERR_NO_WRITE` / `ERR_NO_DELETE` 分派)
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum WriteAction {
39    Insert,
40    Update,
41    Remove,
42}
43
44impl WriteAction {
45    pub fn as_str(&self) -> &'static str {
46        match self {
47            WriteAction::Insert => "insert",
48            WriteAction::Update => "update",
49            WriteAction::Remove => "remove",
50        }
51    }
52}
53
54/// action 字符串 → [`WriteAction`];非法值显式 Err(禁静默回落)
55pub fn write_action_from_str(s: &str) -> Result<WriteAction, String> {
56    match s {
57        "insert" => Ok(WriteAction::Insert),
58        "update" => Ok(WriteAction::Update),
59        "remove" => Ok(WriteAction::Remove),
60        other => Err(format!(
61            "RBAC action \"{other}\" 非法(仅支持 read / insert / update / remove)"
62        )),
63    }
64}
65
66#[derive(Debug, Clone)]
67pub struct Grant {
68    role: String,
69    model: String,        // 精确 schema 名或 "*"(通配)
70    actions: Vec<String>, // 归一化后("write" 已展开),判定用 contains
71    read_fields: Option<HashSet<String>>,
72    write_fields: Option<HashSet<String>>,
73    owner_only: bool,
74    condition: Option<Value>, // 仅等值标量键值(解析期校验)
75}
76
77#[derive(Debug, Clone, Default)]
78pub struct RbacPolicy {
79    mode: RbacMode,
80    managed_roles: HashSet<String>,
81    grants: Vec<Grant>,
82}
83
84/// 单模型判决视图(`decide` 产出)
85#[derive(Debug, Clone)]
86pub struct ModelDecision {
87    pub allowed_actions: HashSet<String>, // read / insert / update / remove
88    /// None = 字段维度不收紧(与 [`crate::permission`] 的 Option 语义一致)
89    pub read_fields: Option<HashSet<String>>,
90    pub write_fields: Option<HashSet<String>>,
91}
92
93/// 拒绝哨兵:复用 `ERR_PERMISSION:` 前缀(宿主零改动映射 403),二级标识 `RBAC:`
94pub fn deny_msg(action: &str, model: &str) -> String {
95    format!("ERR_PERMISSION:RBAC:角色对 [{model}] 无 [{action}] 权限")
96}
97
98// ─── 解析(fail-fast,禁静默吞) ─────────────────────────────
99
100impl RbacPolicy {
101    /// 由策略 JSON 构建;任何非法形态显式 Err,绝不静默回落
102    pub fn from_json(v: &Value) -> Result<Self, String> {
103        let obj = v
104            .as_object()
105            .ok_or_else(|| "RBAC 策略必须是对象".to_string())?;
106
107        let mode = match obj.get("mode") {
108            None | Some(Value::Null) => RbacMode::Overlay,
109            Some(Value::String(s)) if s == "overlay" => RbacMode::Overlay,
110            Some(Value::String(s)) if s == "enforce" => RbacMode::Enforce,
111            Some(other) => {
112                return Err(format!(
113                    "RBAC mode {other} 非法(仅支持 overlay / enforce)"
114                ))
115            }
116        };
117
118        let mut managed_roles = HashSet::new();
119        match obj.get("roles") {
120            None | Some(Value::Null) => {}
121            Some(Value::Object(rm)) => {
122                for (name, _) in rm {
123                    if name.is_empty() {
124                        return Err("RBAC roles 含空角色名".to_string());
125                    }
126                    managed_roles.insert(name.clone());
127                }
128            }
129            Some(other) => return Err(format!("RBAC roles 必须是对象,实际 {other}")),
130        }
131        if mode == RbacMode::Enforce && managed_roles.is_empty() {
132            return Err(
133                "RBAC mode=enforce 时 roles 不能为空(无受管角色 = 策略空转,属配置错误)"
134                    .to_string(),
135            );
136        }
137
138        let mut grants = Vec::new();
139        match obj.get("grants") {
140            None | Some(Value::Null) => {}
141            Some(Value::Array(gs)) => {
142                for (i, g) in gs.iter().enumerate() {
143                    grants.push(parse_grant(i, g)?);
144                }
145            }
146            Some(other) => return Err(format!("RBAC grants 必须是数组,实际 {other}")),
147        }
148
149        Ok(Self {
150            mode,
151            managed_roles,
152            grants,
153        })
154    }
155}
156
157fn parse_grant(idx: usize, v: &Value) -> Result<Grant, String> {
158    let o = v
159        .as_object()
160        .ok_or_else(|| format!("RBAC grants[{idx}] 必须是对象"))?;
161    let get_str = |k: &str| -> Result<String, String> {
162        o.get(k)
163            .and_then(|x| x.as_str())
164            .filter(|s| !s.is_empty())
165            .map(String::from)
166            .ok_or_else(|| format!("RBAC grants[{idx}].{k} 必须是非空字符串"))
167    };
168    let role = get_str("role")?;
169    let model = get_str("model")?;
170
171    let actions_raw = o
172        .get("actions")
173        .and_then(|x| x.as_array())
174        .ok_or_else(|| format!("RBAC grants[{idx}].actions 必须是非空数组"))?;
175    if actions_raw.is_empty() {
176        return Err(format!(
177            "RBAC grants[{idx}].actions 不能为空(不授权请移除该 grant)"
178        ));
179    }
180    let mut actions: Vec<String> = Vec::new();
181    for a in actions_raw {
182        let s = a
183            .as_str()
184            .ok_or_else(|| format!("RBAC grants[{idx}].actions 含非字符串项 {a}"))?;
185        let expanded: &[&str] = match s {
186            "read" => &["read"],
187            "insert" => &["insert"],
188            "update" => &["update"],
189            "remove" => &["remove"],
190            "write" => &["insert", "update", "remove"],
191            other => {
192                return Err(format!(
193                    "RBAC grants[{idx}].actions 含非法值 \"{other}\"(仅支持 read/insert/update/remove/write)"
194                ))
195            }
196        };
197        for e in expanded {
198            if !actions.iter().any(|x| x == e) {
199                actions.push((*e).to_string());
200            }
201        }
202    }
203
204    let parse_fields = |k: &str| -> Result<Option<HashSet<String>>, String> {
205        match o.get(k) {
206            None | Some(Value::Null) => Ok(None),
207            Some(Value::Array(fs)) => {
208                if fs.is_empty() {
209                    return Err(format!(
210                        "RBAC grants[{idx}].{k} 不能为空数组(全禁请移除该 grant)"
211                    ));
212                }
213                let mut set = HashSet::new();
214                for f in fs {
215                    let s = f.as_str().filter(|s| !s.is_empty()).ok_or_else(|| {
216                        format!("RBAC grants[{idx}].{k} 含非法项 {f}(须非空字符串)")
217                    })?;
218                    set.insert(s.to_string());
219                }
220                Ok(Some(set))
221            }
222            Some(other) => Err(format!(
223                "RBAC grants[{idx}].{k} 必须是字符串数组,实际 {other}"
224            )),
225        }
226    };
227    let read_fields = parse_fields("readFields")?;
228    let write_fields = parse_fields("writeFields")?;
229
230    let owner_only = o.get("ownerOnly").map(is_truthy).unwrap_or(false);
231
232    // 第一批仅等值标量条件:键非空且不得以 "$" 开头(操作符/逻辑组禁用),
233    // 值必须是标量——超出形态显式 Err,禁运行时静默失配
234    let condition = match o.get("condition") {
235        None | Some(Value::Null) => None,
236        Some(cm @ Value::Object(_)) => {
237            let entries = cm.as_object().expect("已按 Object 匹配,此处必为对象");
238            for (k, val) in entries {
239                if k.is_empty() || k.starts_with('$') {
240                    return Err(format!(
241                        "RBAC grants[{idx}].condition 键 \"{k}\" 非法(第一批仅支持等值字段条件,键不得以 $ 开头)"
242                    ));
243                }
244                if !val.is_boolean() && !val.is_number() && !val.is_string() && !val.is_null() {
245                    return Err(format!(
246                        "RBAC grants[{idx}].condition[\"{k}\"] 必须是标量(string/number/bool/null),实际 {val}"
247                    ));
248                }
249            }
250            Some(cm.clone())
251        }
252        Some(other) => {
253            return Err(format!(
254                "RBAC grants[{idx}].condition 必须是对象(等值条件),实际 {other}"
255            ))
256        }
257    };
258
259    Ok(Grant {
260        role,
261        model,
262        actions,
263        read_fields,
264        write_fields,
265        owner_only,
266        condition,
267    })
268}
269
270// ─── 判决 ────────────────────────────────────────────────────
271
272/// 有效角色集(对齐 `permission::evaluate`:`roles` 为空回落单 `role`)
273fn effective_roles(c: &Context) -> Vec<String> {
274    match &c.roles {
275        Some(r) if !r.is_empty() => r.clone(),
276        _ => vec![c.role.clone().unwrap_or_default()],
277    }
278}
279
280/// 核心判决:`None` = RBAC 不介入(无 ctx / internal / 豁免角色 / overlay 未覆盖 /
281/// enforce 未受管)。enforce 受管但 matched 为空 → `Some`(全 deny),这是
282/// default deny 的承载点。
283pub fn decide(
284    policy: &RbacPolicy,
285    exempt_roles: &[String],
286    ctx: Option<&Context>,
287    model: &str,
288) -> Option<ModelDecision> {
289    let c = ctx?;
290    if c.internal {
291        return None;
292    }
293    let roles = effective_roles(c);
294    if roles.iter().any(|r| exempt_roles.iter().any(|x| x == r)) {
295        return None;
296    }
297
298    let matched: Vec<&Grant> = policy
299        .grants
300        .iter()
301        .filter(|g| roles.iter().any(|r| r == &g.role) && (g.model == model || g.model == "*"))
302        .collect();
303
304    match policy.mode {
305        RbacMode::Overlay if matched.is_empty() => return None,
306        RbacMode::Enforce => {
307            let managed = roles.iter().any(|r| policy.managed_roles.contains(r));
308            if !managed {
309                return None;
310            }
311        }
312        _ => {}
313    }
314
315    // 字段集:grant 间 OR 语义——任一允许该动作的 grant 未声明字段集 = 授予全字段
316    // (整体不收紧);全部声明了才取并集收紧
317    let mut allowed_actions = HashSet::new();
318    let mut read_all = false;
319    let mut read_union: HashSet<String> = HashSet::new();
320    let mut write_all = false;
321    let mut write_union: HashSet<String> = HashSet::new();
322    for g in &matched {
323        for a in &g.actions {
324            allowed_actions.insert(a.clone());
325        }
326        if g.actions.iter().any(|a| a == "read") {
327            match &g.read_fields {
328                None => read_all = true,
329                Some(s) => read_union.extend(s.iter().cloned()),
330            }
331        }
332        if g.actions
333            .iter()
334            .any(|a| a == "insert" || a == "update" || a == "remove")
335        {
336            match &g.write_fields {
337                None => write_all = true,
338                Some(s) => write_union.extend(s.iter().cloned()),
339            }
340        }
341    }
342    let read_fields = if read_all || read_union.is_empty() {
343        None
344    } else {
345        Some(read_union)
346    };
347    let write_fields = if write_all || write_union.is_empty() {
348        None
349    } else {
350        Some(write_union)
351    };
352    Some(ModelDecision {
353        allowed_actions,
354        read_fields,
355        write_fields,
356    })
357}
358
359// ─── 叠加原语(registry.rbac() == None 时全部直通) ──────────
360
361/// 表级读判定(叠加在 `can_read_schema` 之后)
362pub fn ensure_read(
363    registry: &Registry,
364    schema: &Schema,
365    ctx: Option<&Context>,
366) -> Result<(), String> {
367    let Some(p) = registry.rbac() else {
368        return Ok(());
369    };
370    match decide(p, &registry.role_rules().exempt_roles, ctx, &schema.name) {
371        None => Ok(()),
372        Some(d) if d.allowed_actions.contains("read") => Ok(()),
373        Some(_) => Err(deny_msg("read", &schema.name)),
374    }
375}
376
377/// 表级写判定(叠加在 `can_write_schema` 之后;upsert 路径裁定按 Insert 判)
378pub fn ensure_write(
379    registry: &Registry,
380    schema: &Schema,
381    ctx: Option<&Context>,
382    action: WriteAction,
383) -> Result<(), String> {
384    let Some(p) = registry.rbac() else {
385        return Ok(());
386    };
387    match decide(p, &registry.role_rules().exempt_roles, ctx, &schema.name) {
388        None => Ok(()),
389        Some(d) if d.allowed_actions.contains(action.as_str()) => Ok(()),
390        Some(_) => Err(deny_msg(action.as_str(), &schema.name)),
391    }
392}
393
394/// 探针重入判决(单条 update/remove 的 ownerOnly / condition 命中判定)。
395///
396/// grant 间 OR 语义:任一允许该动作的 grant 无行限制 → 放行;有行限制且命中
397/// doc → 放行;全部不命中 → 拒绝(杜绝静默 0 行,对齐 no-error-masking)。
398pub fn ensure_write_on_doc(
399    registry: &Registry,
400    schema: &Schema,
401    ctx: Option<&Context>,
402    action: WriteAction,
403    doc: &Value,
404) -> Result<(), String> {
405    let Some(p) = registry.rbac() else {
406        return Ok(());
407    };
408    let Some(c) = ctx else { return Ok(()) };
409    if c.internal {
410        return Ok(());
411    }
412    let Some(d) = decide(
413        p,
414        &registry.role_rules().exempt_roles,
415        Some(c),
416        &schema.name,
417    ) else {
418        return Ok(());
419    };
420    if !d.allowed_actions.contains(action.as_str()) {
421        return Err(deny_msg(action.as_str(), &schema.name));
422    }
423    let roles = effective_roles(c);
424    let hit = p.grants.iter().any(|g| {
425        g.actions.iter().any(|a| a == action.as_str())
426            && (g.model == schema.name || g.model == "*")
427            && roles.iter().any(|r| r == &g.role)
428            && match grant_row_restriction(g, c) {
429                None => true,
430                Some(cond) => doc_matches_eq(doc, &cond),
431            }
432    });
433    if hit {
434        Ok(())
435    } else {
436        Err(format!(
437            "ERR_PERMISSION:RBAC:角色对 [{}] 的 [{}] 限于本人/条件内文档",
438            schema.name,
439            action.as_str()
440        ))
441    }
442}
443
444/// 行级条件(grant 间 OR 合并):RBAC 未生效 / 存在无行限制的匹配 grant →
445/// `None`(不收紧);否则各 grant 行限制取 `$or` 串接。
446/// `action` ∈ {"read", "update", "remove"}(insert 无行级语义,调用方不传)。
447pub fn row_condition(
448    registry: &Registry,
449    schema: &Schema,
450    ctx: Option<&Context>,
451    action: &str,
452) -> Option<Value> {
453    let p = registry.rbac()?;
454    let c = ctx?;
455    if c.internal {
456        return None;
457    }
458    decide(
459        p,
460        &registry.role_rules().exempt_roles,
461        Some(c),
462        &schema.name,
463    )?;
464    let roles = effective_roles(c);
465    let mut conds: Vec<Value> = Vec::new();
466    for g in p.grants.iter().filter(|g| {
467        g.actions.iter().any(|a| a == action)
468            && (g.model == schema.name || g.model == "*")
469            && roles.iter().any(|r| r == &g.role)
470    }) {
471        let v = grant_row_restriction(g, c)?;
472        conds.push(v);
473    }
474    match conds.len() {
475        0 => None,
476        1 => Some(conds.pop().unwrap_or(Value::Null)),
477        _ => Some(json!({ "$or": conds })),
478    }
479}
480
481/// 在静态引擎产出的 owner 条件(`merge_owner_condition` 结果)之上叠加 RBAC 行
482/// 条件:两引擎间 `$and`(deny-wins)。任一为 `None` → 取另一者;空对象视为无条件。
483pub fn merge_row_condition(
484    registry: &Registry,
485    schema: &Schema,
486    ctx: Option<&Context>,
487    action: &str,
488    base: Option<Value>,
489) -> Option<Value> {
490    let meaningful = |v: Option<Value>| -> Option<Value> {
491        v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
492    };
493    let rbac_cond = row_condition(registry, schema, ctx, action);
494    match (meaningful(base), rbac_cond) {
495        (None, r) => r,
496        (b, None) => b,
497        (Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
498    }
499}
500
501/// 字段读交集:`base`(静态 `get_readable_fields` 结果)与 RBAC readFields 取交。
502/// `_id` 随 base 保留(文档标识豁免,同 `filter_writable_data` 的 D-03 理由)。
503pub fn overlay_readable_fields(
504    registry: &Registry,
505    model: &str,
506    ctx: Option<&Context>,
507    base: Option<HashSet<String>>,
508) -> Option<HashSet<String>> {
509    let Some(p) = registry.rbac() else {
510        return base;
511    };
512    let Some(d) = decide(p, &registry.role_rules().exempt_roles, ctx, model) else {
513        return base;
514    };
515    let Some(rf) = &d.read_fields else {
516        return base;
517    };
518    match base {
519        None => {
520            let mut s: HashSet<String> = rf.clone();
521            s.insert("_id".to_string());
522            Some(s)
523        }
524        Some(b) => {
525            let keep_id = b.contains("_id");
526            let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
527            if keep_id {
528                s.insert("_id".to_string());
529            }
530            Some(s)
531        }
532    }
533}
534
535/// 字段写交集(语义同 [`overlay_readable_fields`],作用于 writeFields)
536pub fn overlay_writable_fields(
537    registry: &Registry,
538    model: &str,
539    ctx: Option<&Context>,
540    base: Option<HashSet<String>>,
541) -> Option<HashSet<String>> {
542    let Some(p) = registry.rbac() else {
543        return base;
544    };
545    let Some(d) = decide(p, &registry.role_rules().exempt_roles, ctx, model) else {
546        return base;
547    };
548    let Some(wf) = &d.write_fields else {
549        return base;
550    };
551    match base {
552        None => {
553            let mut s: HashSet<String> = wf.clone();
554            s.insert("_id".to_string());
555            Some(s)
556        }
557        Some(b) => {
558            let keep_id = b.contains("_id");
559            let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
560            if keep_id {
561                s.insert("_id".to_string());
562            }
563            Some(s)
564        }
565    }
566}
567
568/// 写入数据过滤(RBAC 感知版,供写路径调用点整体替换
569/// `permission::filter_writable_data`):可写字段 = 静态 writable ∩ RBAC writeFields。
570/// 与 [`crate::permission::filter_writable_data`] 逐行同构(`_id` 豁免、点号按 root),
571/// 仅集合来源叠加 RBAC 交集——静态引擎文件零改动。
572pub fn filter_writable_data_overlay(
573    registry: &Registry,
574    schema: &Schema,
575    ctx: Option<&Context>,
576    data: &Value,
577) -> Value {
578    if registry.rbac().is_none() {
579        return crate::permission::filter_writable_data(registry.role_rules(), schema, ctx, data);
580    }
581    let base = get_writable_fields(registry.role_rules(), schema, ctx);
582    let writable = overlay_writable_fields(registry, &schema.name, ctx, base);
583    let Some(writable) = writable else {
584        return data.clone();
585    };
586    let Some(obj) = data.as_object() else {
587        return data.clone();
588    };
589    let mut result = Map::new();
590    for (key, val) in obj {
591        let root = key.split('.').next().unwrap_or(key);
592        if key == "_id" || writable.contains(root) {
593            result.insert(key.clone(), val.clone());
594        }
595    }
596    Value::Object(result)
597}
598
599/// 单字段可读判定(RBAC 感知版):静态 `is_field_readable` ∧ RBAC readFields。
600/// 点号路径按 root 字段判定(与静态语义一致);`ctx=None` / internal / RBAC 未生效 → 放行。
601pub fn is_field_readable_overlay(
602    registry: &Registry,
603    schema: &Schema,
604    ctx: Option<&Context>,
605    field: &str,
606) -> bool {
607    if !crate::permission::is_field_readable(registry.role_rules(), schema, ctx, field) {
608        return false;
609    }
610    let Some(p) = registry.rbac() else {
611        return true;
612    };
613    let Some(c) = ctx else { return true };
614    if c.internal {
615        return true;
616    }
617    let Some(d) = decide(
618        p,
619        &registry.role_rules().exempt_roles,
620        Some(c),
621        &schema.name,
622    ) else {
623        return true;
624    };
625    match &d.read_fields {
626        None => true,
627        Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
628    }
629}
630
631/// 探针命令需额外投影的字段(单条 update/remove 的 RBAC 行级判定依赖):
632/// 各 grant condition 的字段键(ownerOnly 的 `createdBy` 由调用方静态并入)。
633/// 无 RBAC 行级限制 → 空表(调用方维持既有 `{_id, createdBy}` 投影)。
634pub fn probe_condition_fields(
635    registry: &Registry,
636    schema: &Schema,
637    ctx: Option<&Context>,
638    action: &str,
639) -> Vec<String> {
640    let Some(p) = registry.rbac() else {
641        return Vec::new();
642    };
643    let Some(c) = ctx else { return Vec::new() };
644    if c.internal {
645        return Vec::new();
646    }
647    if decide(
648        p,
649        &registry.role_rules().exempt_roles,
650        Some(c),
651        &schema.name,
652    )
653    .is_none()
654    {
655        return Vec::new();
656    }
657    let roles = effective_roles(c);
658    let mut fields = Vec::new();
659    for g in p.grants.iter().filter(|g| {
660        g.actions.iter().any(|a| a == action)
661            && (g.model == schema.name || g.model == "*")
662            && roles.iter().any(|r| r == &g.role)
663    }) {
664        if let Some(cond) = &g.condition {
665            if let Some(obj) = cond.as_object() {
666                for k in obj.keys() {
667                    if !fields.iter().any(|x: &String| x == k) {
668                        fields.push(k.clone());
669                    }
670                }
671            }
672        }
673    }
674    fields
675}
676
677// ─── 内部工具 ────────────────────────────────────────────────
678
679/// 单 grant 的行限制:`None` = 无限制;`ownerOnly` → `{createdBy: userId}`;
680/// `condition` → 原样;两者并存 → `$and`。`ownerOnly` 但 userId 缺失 → 恒假哨兵
681/// (禁当 `None`——那是静默放大)。
682fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
683    if !g.owner_only && g.condition.is_none() {
684        return None;
685    }
686    let owner = if g.owner_only {
687        let uid = c.user_id.clone().unwrap_or_default();
688        if uid.is_empty() {
689            // 恒假条件:__rbac_impossible__ 不可能在文档出现 → doc_matches_eq 恒 false
690            Some(json!({ "__rbac_impossible__": true }))
691        } else {
692            Some(json!({ "createdBy": uid }))
693        }
694    } else {
695        None
696    };
697    match (owner, g.condition.clone()) {
698        (None, cond) => cond,
699        (owner, None) => owner,
700        (Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
701    }
702}
703
704/// 等值条件命中判定(`cond` 为 `{字段: 标量}`,解析期已保证);哨兵键恒不命中
705fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
706    let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
707        return false;
708    };
709    cond_map
710        .iter()
711        .all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
712}