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` / `super_admin` / `admin` → 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("RBAC mode=enforce 时 roles 不能为空(无受管角色 = 策略空转,属配置错误)".to_string());
133        }
134
135        let mut grants = Vec::new();
136        match obj.get("grants") {
137            None | Some(Value::Null) => {}
138            Some(Value::Array(gs)) => {
139                for (i, g) in gs.iter().enumerate() {
140                    grants.push(parse_grant(i, g)?);
141                }
142            }
143            Some(other) => return Err(format!("RBAC grants 必须是数组,实际 {other}")),
144        }
145
146        Ok(Self { mode, managed_roles, grants })
147    }
148}
149
150fn parse_grant(idx: usize, v: &Value) -> Result<Grant, String> {
151    let o = v
152        .as_object()
153        .ok_or_else(|| format!("RBAC grants[{idx}] 必须是对象"))?;
154    let get_str = |k: &str| -> Result<String, String> {
155        o.get(k)
156            .and_then(|x| x.as_str())
157            .filter(|s| !s.is_empty())
158            .map(String::from)
159            .ok_or_else(|| format!("RBAC grants[{idx}].{k} 必须是非空字符串"))
160    };
161    let role = get_str("role")?;
162    let model = get_str("model")?;
163
164    let actions_raw = o
165        .get("actions")
166        .and_then(|x| x.as_array())
167        .ok_or_else(|| format!("RBAC grants[{idx}].actions 必须是非空数组"))?;
168    if actions_raw.is_empty() {
169        return Err(format!("RBAC grants[{idx}].actions 不能为空(不授权请移除该 grant)"));
170    }
171    let mut actions: Vec<String> = Vec::new();
172    for a in actions_raw {
173        let s = a
174            .as_str()
175            .ok_or_else(|| format!("RBAC grants[{idx}].actions 含非字符串项 {a}"))?;
176        let expanded: &[&str] = match s {
177            "read" => &["read"],
178            "insert" => &["insert"],
179            "update" => &["update"],
180            "remove" => &["remove"],
181            "write" => &["insert", "update", "remove"],
182            other => {
183                return Err(format!(
184                    "RBAC grants[{idx}].actions 含非法值 \"{other}\"(仅支持 read/insert/update/remove/write)"
185                ))
186            }
187        };
188        for e in expanded {
189            if !actions.iter().any(|x| x == e) {
190                actions.push((*e).to_string());
191            }
192        }
193    }
194
195    let parse_fields = |k: &str| -> Result<Option<HashSet<String>>, String> {
196        match o.get(k) {
197            None | Some(Value::Null) => Ok(None),
198            Some(Value::Array(fs)) => {
199                if fs.is_empty() {
200                    return Err(format!(
201                        "RBAC grants[{idx}].{k} 不能为空数组(全禁请移除该 grant)"
202                    ));
203                }
204                let mut set = HashSet::new();
205                for f in fs {
206                    let s = f
207                        .as_str()
208                        .filter(|s| !s.is_empty())
209                        .ok_or_else(|| format!("RBAC grants[{idx}].{k} 含非法项 {f}(须非空字符串)"))?;
210                    set.insert(s.to_string());
211                }
212                Ok(Some(set))
213            }
214            Some(other) => Err(format!("RBAC grants[{idx}].{k} 必须是字符串数组,实际 {other}")),
215        }
216    };
217    let read_fields = parse_fields("readFields")?;
218    let write_fields = parse_fields("writeFields")?;
219
220    let owner_only = o.get("ownerOnly").map(is_truthy).unwrap_or(false);
221
222    // 第一批仅等值标量条件:键非空且不得以 "$" 开头(操作符/逻辑组禁用),
223    // 值必须是标量——超出形态显式 Err,禁运行时静默失配
224    let condition = match o.get("condition") {
225        None | Some(Value::Null) => None,
226        Some(cm @ Value::Object(_)) => {
227            let entries = cm
228                .as_object()
229                .expect("已按 Object 匹配,此处必为对象");
230            for (k, val) in entries {
231                if k.is_empty() || k.starts_with('$') {
232                    return Err(format!(
233                        "RBAC grants[{idx}].condition 键 \"{k}\" 非法(第一批仅支持等值字段条件,键不得以 $ 开头)"
234                    ));
235                }
236                if !val.is_boolean() && !val.is_number() && !val.is_string() && !val.is_null() {
237                    return Err(format!(
238                        "RBAC grants[{idx}].condition[\"{k}\"] 必须是标量(string/number/bool/null),实际 {val}"
239                    ));
240                }
241            }
242            Some(cm.clone())
243        }
244        Some(other) => {
245            return Err(format!(
246                "RBAC grants[{idx}].condition 必须是对象(等值条件),实际 {other}"
247            ))
248        }
249    };
250
251    Ok(Grant { role, model, actions, read_fields, write_fields, owner_only, condition })
252}
253
254// ─── 判决 ────────────────────────────────────────────────────
255
256/// 有效角色集(对齐 `permission::evaluate`:`roles` 为空回落单 `role`)
257fn effective_roles(c: &Context) -> Vec<String> {
258    match &c.roles {
259        Some(r) if !r.is_empty() => r.clone(),
260        _ => vec![c.role.clone().unwrap_or_default()],
261    }
262}
263
264/// 核心判决:`None` = RBAC 不介入(无 ctx / internal / 豁免角色 / overlay 未覆盖 /
265/// enforce 未受管)。enforce 受管但 matched 为空 → `Some`(全 deny),这是
266/// default deny 的承载点。
267pub fn decide(policy: &RbacPolicy, ctx: Option<&Context>, model: &str) -> Option<ModelDecision> {
268    let c = ctx?;
269    if c.internal {
270        return None;
271    }
272    let roles = effective_roles(c);
273    if roles.iter().any(|r| r == "super_admin" || r == "admin") {
274        return None;
275    }
276
277    let matched: Vec<&Grant> = policy
278        .grants
279        .iter()
280        .filter(|g| roles.iter().any(|r| r == &g.role) && (g.model == model || g.model == "*"))
281        .collect();
282
283    match policy.mode {
284        RbacMode::Overlay if matched.is_empty() => return None,
285        RbacMode::Enforce => {
286            let managed = roles.iter().any(|r| policy.managed_roles.contains(r));
287            if !managed {
288                return None;
289            }
290        }
291        _ => {}
292    }
293
294    // 字段集:grant 间 OR 语义——任一允许该动作的 grant 未声明字段集 = 授予全字段
295    // (整体不收紧);全部声明了才取并集收紧
296    let mut allowed_actions = HashSet::new();
297    let mut read_all = false;
298    let mut read_union: HashSet<String> = HashSet::new();
299    let mut write_all = false;
300    let mut write_union: HashSet<String> = HashSet::new();
301    for g in &matched {
302        for a in &g.actions {
303            allowed_actions.insert(a.clone());
304        }
305        if g.actions.iter().any(|a| a == "read") {
306            match &g.read_fields {
307                None => read_all = true,
308                Some(s) => read_union.extend(s.iter().cloned()),
309            }
310        }
311        if g
312            .actions
313            .iter()
314            .any(|a| a == "insert" || a == "update" || a == "remove")
315        {
316            match &g.write_fields {
317                None => write_all = true,
318                Some(s) => write_union.extend(s.iter().cloned()),
319            }
320        }
321    }
322    let read_fields = if read_all || read_union.is_empty() { None } else { Some(read_union) };
323    let write_fields = if write_all || write_union.is_empty() { None } else { Some(write_union) };
324    Some(ModelDecision { allowed_actions, read_fields, write_fields })
325}
326
327// ─── 叠加原语(registry.rbac() == None 时全部直通) ──────────
328
329/// 表级读判定(叠加在 `can_read_schema` 之后)
330pub fn ensure_read(
331    registry: &Registry,
332    schema: &Schema,
333    ctx: Option<&Context>,
334) -> Result<(), String> {
335    let Some(p) = registry.rbac() else { return Ok(()) };
336    match decide(p, ctx, &schema.name) {
337        None => Ok(()),
338        Some(d) if d.allowed_actions.contains("read") => Ok(()),
339        Some(_) => Err(deny_msg("read", &schema.name)),
340    }
341}
342
343/// 表级写判定(叠加在 `can_write_schema` 之后;upsert 路径裁定按 Insert 判)
344pub fn ensure_write(
345    registry: &Registry,
346    schema: &Schema,
347    ctx: Option<&Context>,
348    action: WriteAction,
349) -> Result<(), String> {
350    let Some(p) = registry.rbac() else { return Ok(()) };
351    match decide(p, ctx, &schema.name) {
352        None => Ok(()),
353        Some(d) if d.allowed_actions.contains(action.as_str()) => Ok(()),
354        Some(_) => Err(deny_msg(action.as_str(), &schema.name)),
355    }
356}
357
358/// 探针重入判决(单条 update/remove 的 ownerOnly / condition 命中判定)。
359///
360/// grant 间 OR 语义:任一允许该动作的 grant 无行限制 → 放行;有行限制且命中
361/// doc → 放行;全部不命中 → 拒绝(杜绝静默 0 行,对齐 no-error-masking)。
362pub fn ensure_write_on_doc(
363    registry: &Registry,
364    schema: &Schema,
365    ctx: Option<&Context>,
366    action: WriteAction,
367    doc: &Value,
368) -> Result<(), String> {
369    let Some(p) = registry.rbac() else { return Ok(()) };
370    let Some(c) = ctx else { return Ok(()) };
371    if c.internal {
372        return Ok(());
373    }
374    let Some(d) = decide(p, Some(c), &schema.name) else { return Ok(()) };
375    if !d.allowed_actions.contains(action.as_str()) {
376        return Err(deny_msg(action.as_str(), &schema.name));
377    }
378    let roles = effective_roles(c);
379    let hit = p.grants.iter().any(|g| {
380        g.actions.iter().any(|a| a == action.as_str())
381            && (g.model == schema.name || g.model == "*")
382            && roles.iter().any(|r| r == &g.role)
383            && match grant_row_restriction(g, c) {
384                None => true,
385                Some(cond) => doc_matches_eq(doc, &cond),
386            }
387    });
388    if hit {
389        Ok(())
390    } else {
391        Err(format!(
392            "ERR_PERMISSION:RBAC:角色对 [{}] 的 [{}] 限于本人/条件内文档",
393            schema.name,
394            action.as_str()
395        ))
396    }
397}
398
399/// 行级条件(grant 间 OR 合并):RBAC 未生效 / 存在无行限制的匹配 grant →
400/// `None`(不收紧);否则各 grant 行限制取 `$or` 串接。
401/// `action` ∈ {"read", "update", "remove"}(insert 无行级语义,调用方不传)。
402pub fn row_condition(
403    registry: &Registry,
404    schema: &Schema,
405    ctx: Option<&Context>,
406    action: &str,
407) -> Option<Value> {
408    let p = registry.rbac()?;
409    let c = ctx?;
410    if c.internal {
411        return None;
412    }
413    decide(p, Some(c), &schema.name)?;
414    let roles = effective_roles(c);
415    let mut conds: Vec<Value> = Vec::new();
416    for g in p.grants.iter().filter(|g| {
417        g.actions.iter().any(|a| a == action)
418            && (g.model == schema.name || g.model == "*")
419            && roles.iter().any(|r| r == &g.role)
420    }) {
421        match grant_row_restriction(g, c) {
422            None => return None, // 任一 grant 无行限制 → 无限制
423            Some(v) => conds.push(v),
424        }
425    }
426    match conds.len() {
427        0 => None,
428        1 => Some(conds.pop().unwrap_or(Value::Null)),
429        _ => Some(json!({ "$or": conds })),
430    }
431}
432
433/// 在静态引擎产出的 owner 条件(`merge_owner_condition` 结果)之上叠加 RBAC 行
434/// 条件:两引擎间 `$and`(deny-wins)。任一为 `None` → 取另一者;空对象视为无条件。
435pub fn merge_row_condition(
436    registry: &Registry,
437    schema: &Schema,
438    ctx: Option<&Context>,
439    action: &str,
440    base: Option<Value>,
441) -> Option<Value> {
442    let meaningful = |v: Option<Value>| -> Option<Value> {
443        v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
444    };
445    let rbac_cond = row_condition(registry, schema, ctx, action);
446    match (meaningful(base), rbac_cond) {
447        (None, r) => r,
448        (b, None) => b,
449        (Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
450    }
451}
452
453/// 字段读交集:`base`(静态 `get_readable_fields` 结果)与 RBAC readFields 取交。
454/// `_id` 随 base 保留(文档标识豁免,同 `filter_writable_data` 的 D-03 理由)。
455pub fn overlay_readable_fields(
456    p: Option<&RbacPolicy>,
457    model: &str,
458    ctx: Option<&Context>,
459    base: Option<HashSet<String>>,
460) -> Option<HashSet<String>> {
461    let Some(p) = p else { return base };
462    let Some(d) = decide(p, ctx, model) else { return base };
463    let Some(rf) = &d.read_fields else { return base };
464    match base {
465        None => {
466            let mut s: HashSet<String> = rf.clone();
467            s.insert("_id".to_string());
468            Some(s)
469        }
470        Some(b) => {
471            let keep_id = b.contains("_id");
472            let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
473            if keep_id {
474                s.insert("_id".to_string());
475            }
476            Some(s)
477        }
478    }
479}
480
481/// 字段写交集(语义同 [`overlay_readable_fields`],作用于 writeFields)
482pub fn overlay_writable_fields(
483    p: Option<&RbacPolicy>,
484    model: &str,
485    ctx: Option<&Context>,
486    base: Option<HashSet<String>>,
487) -> Option<HashSet<String>> {
488    let Some(p) = p else { return base };
489    let Some(d) = decide(p, ctx, model) else { return base };
490    let Some(wf) = &d.write_fields else { return base };
491    match base {
492        None => {
493            let mut s: HashSet<String> = wf.clone();
494            s.insert("_id".to_string());
495            Some(s)
496        }
497        Some(b) => {
498            let keep_id = b.contains("_id");
499            let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
500            if keep_id {
501                s.insert("_id".to_string());
502            }
503            Some(s)
504        }
505    }
506}
507
508/// 写入数据过滤(RBAC 感知版,供写路径调用点整体替换
509/// `permission::filter_writable_data`):可写字段 = 静态 writable ∩ RBAC writeFields。
510/// 与 [`crate::permission::filter_writable_data`] 逐行同构(`_id` 豁免、点号按 root),
511/// 仅集合来源叠加 RBAC 交集——静态引擎文件零改动。
512pub fn filter_writable_data_overlay(
513    registry: &Registry,
514    schema: &Schema,
515    ctx: Option<&Context>,
516    data: &Value,
517) -> Value {
518    let Some(p) = registry.rbac() else {
519        return crate::permission::filter_writable_data(schema, ctx, data);
520    };
521    let base = get_writable_fields(schema, ctx);
522    let writable = overlay_writable_fields(Some(p), &schema.name, ctx, base);
523    let Some(writable) = writable else {
524        return data.clone();
525    };
526    let Some(obj) = data.as_object() else {
527        return data.clone();
528    };
529    let mut result = Map::new();
530    for (key, val) in obj {
531        let root = key.split('.').next().unwrap_or(key);
532        if key == "_id" || writable.contains(root) {
533            result.insert(key.clone(), val.clone());
534        }
535    }
536    Value::Object(result)
537}
538
539/// 单字段可读判定(RBAC 感知版):静态 `is_field_readable` ∧ RBAC readFields。
540/// 点号路径按 root 字段判定(与静态语义一致);`ctx=None` / internal / RBAC 未生效 → 放行。
541pub fn is_field_readable_overlay(
542    registry: &Registry,
543    schema: &Schema,
544    ctx: Option<&Context>,
545    field: &str,
546) -> bool {
547    if !crate::permission::is_field_readable(schema, ctx, field) {
548        return false;
549    }
550    let Some(p) = registry.rbac() else { return true };
551    let Some(c) = ctx else { return true };
552    if c.internal {
553        return true;
554    }
555    let Some(d) = decide(p, Some(c), &schema.name) else { return true };
556    match &d.read_fields {
557        None => true,
558        Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
559    }
560}
561
562/// 探针命令需额外投影的字段(单条 update/remove 的 RBAC 行级判定依赖):
563/// 各 grant condition 的字段键(ownerOnly 的 `createdBy` 由调用方静态并入)。
564/// 无 RBAC 行级限制 → 空表(调用方维持既有 `{_id, createdBy}` 投影)。
565pub fn probe_condition_fields(
566    registry: &Registry,
567    schema: &Schema,
568    ctx: Option<&Context>,
569    action: &str,
570) -> Vec<String> {
571    let Some(p) = registry.rbac() else { return Vec::new() };
572    let Some(c) = ctx else { return Vec::new() };
573    if c.internal {
574        return Vec::new();
575    }
576    if decide(p, Some(c), &schema.name).is_none() {
577        return Vec::new();
578    }
579    let roles = effective_roles(c);
580    let mut fields = Vec::new();
581    for g in p.grants.iter().filter(|g| {
582        g.actions.iter().any(|a| a == action)
583            && (g.model == schema.name || g.model == "*")
584            && roles.iter().any(|r| r == &g.role)
585    }) {
586        if let Some(cond) = &g.condition {
587            if let Some(obj) = cond.as_object() {
588                for k in obj.keys() {
589                    if !fields.iter().any(|x: &String| x == k) {
590                        fields.push(k.clone());
591                    }
592                }
593            }
594        }
595    }
596    fields
597}
598
599// ─── 内部工具 ────────────────────────────────────────────────
600
601/// 单 grant 的行限制:`None` = 无限制;`ownerOnly` → `{createdBy: userId}`;
602/// `condition` → 原样;两者并存 → `$and`。`ownerOnly` 但 userId 缺失 → 恒假哨兵
603/// (禁当 `None`——那是静默放大)。
604fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
605    if !g.owner_only && g.condition.is_none() {
606        return None;
607    }
608    let owner = if g.owner_only {
609        let uid = c.user_id.clone().unwrap_or_default();
610        if uid.is_empty() {
611            // 恒假条件:__rbac_impossible__ 不可能在文档出现 → doc_matches_eq 恒 false
612            Some(json!({ "__rbac_impossible__": true }))
613        } else {
614            Some(json!({ "createdBy": uid }))
615        }
616    } else {
617        None
618    };
619    match (owner, g.condition.clone()) {
620        (None, cond) => cond,
621        (owner, None) => owner,
622        (Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
623    }
624}
625
626/// 等值条件命中判定(`cond` 为 `{字段: 标量}`,解析期已保证);哨兵键恒不命中
627fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
628    let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
629        return false;
630    };
631    cond_map.iter().all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
632}