Skip to main content

rust_store_core/
permission.rs

1//! 权限引擎(对应 JS `src/permission.js`)
2//!
3//! 与 JS 版的差异:`AsyncLocalStorage` 隐式上下文改为**显式 `Context` 入参**。
4//!
5//! ## 信任模型(fail-open 契约)
6//!
7//! `ctx: None` = 「系统内部调用,跳过权限检查」——与 JS 原版 `if (ctx)` 语义对齐,
8//! **默认 fail-open**(调用方忘传 ctx 不会报错而是放行)。对安全敏感的宿主应:
9//!
10//! 1. 开启 [`crate::schema::Registry::set_require_context(true)`]:此后 `ctx` 缺失
11//!    在所有 plan 入口显式报错(fail-secure opt-in,默认关闭以保持三端 parity);
12//! 2. 内部调用(索引创建、归档回填等)显式传 [`Context::system`],
13//!    与「忘传 ctx」在语义上彻底分离。
14
15use std::collections::HashSet;
16
17use serde_json::{json, Map, Value};
18
19use crate::schema::Schema;
20use crate::types::{is_truthy, str_list};
21
22#[derive(Debug, Clone, Default)]
23pub struct Context {
24    pub user_id: Option<String>,
25    pub roles: Option<Vec<String>>,
26    pub role: Option<String>,
27    pub internal: bool,
28}
29
30impl Context {
31    /// 系统内部调用上下文:`internal: true` → 权限引擎全放行、不注入 owner 条件。
32    ///
33    /// 供 Host 的内部路径(索引创建、归档回填、后台任务等)显式表达「这是系统调用」,
34    /// 与 `None`(调用方未传上下文,`require_context` 开启时会报错)区分开。
35    pub fn system() -> Self {
36        Self {
37            user_id: None,
38            roles: None,
39            role: None,
40            internal: true,
41        }
42    }
43}
44
45/// 从 fixture/请求的 JSON 上下文构建 `Context`;null/非对象 → None(等价 JS 的 undefined)
46pub fn context_from_value(v: &Value) -> Option<Context> {
47    if v.is_null() {
48        return None;
49    }
50    let o = v.as_object()?;
51    Some(Context {
52        user_id: o.get("userId").and_then(|x| x.as_str()).map(String::from),
53        roles: str_list(o.get("roles")),
54        role: o.get("role").and_then(|x| x.as_str()).map(String::from),
55        internal: o.get("internal").map(is_truthy).unwrap_or(false),
56    })
57}
58
59/// doc 语义(用于 creator 伪角色):
60/// `Missing` → 新插入(无已有文档,creator 自动通过)
61/// `NoResult` → 查询无结果(creator 不通过)
62/// `Doc` → 已有文档
63#[derive(Debug, Clone, Copy)]
64pub enum Doc<'a> {
65    Missing,
66    NoResult,
67    Doc(&'a Value),
68}
69
70/// schema 白名单缺失/为空时的默认姿态(设计 §11.2)。
71/// Open = 现状放行语义(guest 读拒已随清单化移除);Closed = fail-secure(未配置模型读写全拒)。
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum UnconfiguredPolicy {
74    #[default]
75    Open,
76    Closed,
77}
78
79impl UnconfiguredPolicy {
80    /// 字符串 → 姿态;未知值 **Err**(禁静默回落 Open,对齐 `Profile::from_str_or_err`)
81    pub fn from_str_or_err(s: &str) -> Result<Self, String> {
82        match s {
83            "open" => Ok(UnconfiguredPolicy::Open),
84            "closed" => Ok(UnconfiguredPolicy::Closed),
85            other => Err(format!(
86                "未知 unconfigured_policy: {other}(仅 open / closed)"
87            )),
88        }
89    }
90    pub fn as_str(&self) -> &'static str {
91        match self {
92            UnconfiguredPolicy::Open => "open",
93            UnconfiguredPolicy::Closed => "closed",
94        }
95    }
96}
97
98/// 角色清单与未配置姿态(判决的全部用户配置输入,无任何隐藏项——设计 §11.3)。
99/// 默认值即「无豁免 / 无拒写 / Open」:清单化后判决 = 白名单 ∧ RBAC 策略 ∧ 本规则。
100#[derive(Debug, Clone, Default)]
101pub struct RoleRules {
102    /// 豁免角色清单:命中者在一切判决环节(静态 + RBAC)直接放行。默认空。
103    pub exempt_roles: Vec<String>,
104    /// 拒写角色清单:命中者一切写路径拒绝(读不受影响)。默认空。
105    pub deny_write_roles: Vec<String>,
106    /// 未配置姿态。默认 Open。
107    pub unconfigured: UnconfiguredPolicy,
108}
109
110/// 定义层(register / 覆盖)门禁策略(分步 03)。
111///
112/// - `closed = false`(缺省):**Open** —— 全放行,保持既有 parity(harness / 测试零变更);
113/// - `closed = true`:**Closed** —— 仅 `internal` 或 `roles` 白名单命中者可注册/覆盖。
114#[derive(Debug, Clone, Default)]
115pub struct MetaPolicy {
116    /// 是否启用门禁(false = Open,缺省)。
117    pub closed: bool,
118    /// Closed 时的授权角色白名单(空 = 仅 internal 可注册)。
119    pub roles: Vec<String>,
120}
121
122/// 定义层判决:Open 全放行;Closed 仅 `internal` 或白名单角色(有效角色集,与清单语义一致)。
123///
124/// `ctx = None` 在 Closed 下**拒绝**(fail-secure);拒绝由调用点(`register_with_ctx`)
125/// 转为显式 `ERR_PERMISSION:` 错误,不静默放行。
126pub fn can_register(policy: &MetaPolicy, ctx: Option<&Context>) -> bool {
127    if !policy.closed {
128        return true;
129    }
130    match ctx {
131        None => false,
132        Some(c) => c.internal || has_any(c, &policy.roles),
133    }
134}
135
136/// 评估当前用户是否满足指定角色白名单。
137///
138/// 判决输入全部来自 `rules`(用户配置,默认 []/[]/Open),无任何内置角色硬编码:
139/// - 豁免:有效角色 ∩ `rules.exempt_roles` ≠ ∅ → true(默认空 = 无豁免)。豁免检查
140///   **先于**未配置姿态——设计 §11.3「一切判决环节直通」:`set_exempt_roles` 与
141///   `Closed` 并存时豁免仍生效(豁免是显式信任声明,姿态管不住它);
142/// - 未配置(role_list 缺失/空):`Open` 放行(guest 读拒已随清单化移除——迁移差异,
143///   设计 §11.4);`Closed` 对非内部用户全拒(fail-secure);ctx=None 与 internal
144///   的直通不受姿态影响(无 ctx 维度归 `require_context` 管,设计 §11.6)。
145pub fn evaluate(
146    rules: &RoleRules,
147    ctx: Option<&Context>,
148    role_list: Option<&[String]>,
149    doc: Doc,
150    owner_key: &str,
151) -> bool {
152    let Some(c) = ctx else { return true };
153    if c.internal {
154        return true;
155    }
156    // 豁免清单(默认空——无隐形豁免;清单化语义见设计 §11.3)
157    if has_any(c, &rules.exempt_roles) {
158        return true;
159    }
160    let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
161    if empty {
162        // 未配置姿态显式化(原 empty 分支的 guest 读拒随清单化移除)
163        return rules.unconfigured == UnconfiguredPolicy::Open;
164    }
165
166    // ① 角色匹配
167    let effective = effective(c);
168    // `empty` 已早退,此处 role_list 必为 Some 且非空
169    let Some(list) = role_list else { return true };
170    if list.iter().any(|r| effective.iter().any(|x| x == r)) {
171        return true;
172    }
173
174    // ② 创作者匹配(归属键由 schema.ownerKey 声明,缺省 createdBy)
175    match_creator(c, doc, list, owner_key)
176}
177
178fn match_creator(ctx: &Context, doc: Doc, role_list: &[String], owner_key: &str) -> bool {
179    if !role_list.iter().any(|r| r == "creator") {
180        return false;
181    }
182    match doc {
183        Doc::Missing => true,
184        Doc::NoResult => false,
185        Doc::Doc(d) => {
186            let uid = ctx.user_id.clone().unwrap_or_default();
187            if uid.is_empty() {
188                return false;
189            }
190            d.get(owner_key)
191                .and_then(|v| v.as_str())
192                .map(|cb| cb == uid)
193                .unwrap_or(false)
194        }
195    }
196}
197
198// ─── 字段级过滤(读) ─────────────────────────────────────────
199
200/// ctx 为 None 时返回 None(= 不做裁剪)
201pub fn get_readable_fields(
202    rules: &RoleRules,
203    schema: &Schema,
204    ctx: Option<&Context>,
205) -> Option<HashSet<String>> {
206    let c = ctx?;
207    let mut allowed = HashSet::new();
208    for (key, field) in &schema.fields {
209        match &field.read {
210            Some(rl) => {
211                if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
212                    allowed.insert(key.clone());
213                }
214            }
215            None => {
216                allowed.insert(key.clone());
217            }
218        }
219    }
220    Some(allowed)
221}
222
223pub fn get_readable_computes(
224    rules: &RoleRules,
225    schema: &Schema,
226    ctx: Option<&Context>,
227) -> Option<HashSet<String>> {
228    let c = ctx?;
229    let mut allowed = HashSet::new();
230    for (key, comp) in &schema.computes {
231        match &comp.read {
232            Some(rl) => {
233                if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
234                    allowed.insert(key.clone());
235                }
236            }
237            None => {
238                allowed.insert(key.clone());
239            }
240        }
241    }
242    Some(allowed)
243}
244
245pub fn get_readable_relations(
246    rules: &RoleRules,
247    schema: &Schema,
248    ctx: Option<&Context>,
249) -> Option<HashSet<String>> {
250    let c = ctx?;
251    let mut allowed = HashSet::new();
252    for (key, rel) in &schema.relations {
253        match &rel.read {
254            Some(rl) => {
255                if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
256                    allowed.insert(key.clone());
257                }
258            }
259            None => {
260                allowed.insert(key.clone());
261            }
262        }
263    }
264    Some(allowed)
265}
266
267/// 单个字段读权限判定(点号路径按 root 字段判定)。
268///
269/// - `ctx = None` → 放行(fail-open,与默认姿态一致);
270/// - 字段未在 schema `fields` 中声明 → 放行(无 `field.read` 配置可依);
271/// - 字段在 schema 中且配了 `read` 白名单 → 按 [`get_readable_fields`] 判定。
272pub fn is_field_readable(
273    rules: &RoleRules,
274    schema: &Schema,
275    ctx: Option<&Context>,
276    field: &str,
277) -> bool {
278    let root = field.split('.').next().unwrap_or(field);
279    if !schema.fields.contains_key(root) {
280        return true;
281    }
282    match get_readable_fields(rules, schema, ctx) {
283        None => true,
284        Some(allowed) => allowed.contains(root),
285    }
286}
287
288/// 单个关系读权限判定(R6/L1)。`ctx = None` → 放行(fail-open)。
289pub fn is_relation_readable(
290    rules: &RoleRules,
291    schema: &Schema,
292    ctx: Option<&Context>,
293    rel: &str,
294) -> bool {
295    match get_readable_relations(rules, schema, ctx) {
296        None => true,
297        Some(allowed) => allowed.contains(rel),
298    }
299}
300
301// ─── Schema 级检查 ────────────────────────────────────────────
302
303pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
304    evaluate(
305        rules,
306        ctx,
307        schema.read.as_deref(),
308        Doc::Missing,
309        schema.owner_key(),
310    )
311}
312
313/// 拒写清单命中者一切写路径拒绝(读不受影响,默认空——无拒写);
314/// R2 语义保留:write 显式配置为空白名单(`[]`)= 未声明任何可写角色 → 拒绝一切写,
315/// 豁免清单命中者与 internal 例外。
316pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
317    if let Some(c) = ctx {
318        if has_any(c, &rules.deny_write_roles) {
319            return false;
320        }
321        if !c.internal
322            && matches!(schema.write, Some(ref w) if w.is_empty())
323            && !has_any(c, &rules.exempt_roles)
324        {
325            return false;
326        }
327    }
328    evaluate(
329        rules,
330        ctx,
331        schema.write.as_deref(),
332        Doc::Missing,
333        schema.owner_key(),
334    )
335}
336
337/// 有效角色集:`roles` 非空用之,否则回落单 `role`——对齐 `rbac::effective_roles`
338/// (rbac.rs)与白名单匹配语义。清单命中一律用本函数(不再存在
339/// 「roles 为空则清单失明」的隐形差异)。
340fn effective(c: &Context) -> Vec<String> {
341    match &c.roles {
342        Some(r) if !r.is_empty() => r.clone(),
343        _ => vec![c.role.clone().unwrap_or_default()],
344    }
345}
346
347/// 有效角色集与清单是否有交集
348fn has_any(c: &Context, list: &[String]) -> bool {
349    effective(c).iter().any(|r| list.iter().any(|x| x == r))
350}
351
352/// [`has_any`] 的 crate 内公开包装:command 层写路径复用,避免各调用点手写 effective 归一
353pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
354    has_any(c, list)
355}
356
357// ─── 所有者条件注入 ──────────────────────────────────────────
358
359pub fn should_inject_owner_condition(
360    rules: &RoleRules,
361    schema: &Schema,
362    ctx: Option<&Context>,
363) -> bool {
364    let Some(c) = ctx else { return false };
365    // JS `!ctx.userId`:空串同样视为缺席
366    if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
367        return false;
368    }
369    if c.internal {
370        return false;
371    }
372    if has_any(c, &rules.exempt_roles) {
373        return false;
374    }
375    let effective = effective(c);
376    let read = schema.read.clone().unwrap_or_default();
377    let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
378    if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
379        return false;
380    }
381    read.iter().any(|r| r == "creator")
382}
383
384/// 非 admin 用户只看自己的数据:`creator` 专属读权限时注入归属键条件
385/// (键取 [`Schema::owner_key`],缺省 `createdBy`)
386pub fn merge_owner_condition(
387    rules: &RoleRules,
388    schema: &Schema,
389    ctx: Option<&Context>,
390    condition: Option<Value>,
391) -> Option<Value> {
392    if !should_inject_owner_condition(rules, schema, ctx) {
393        return condition;
394    }
395    let mut owner = Map::new();
396    owner.insert(schema.owner_key().to_string(), json!(ctx?.user_id));
397    let owner = Value::Object(owner);
398    match condition.filter(is_truthy) {
399        None => Some(owner),
400        Some(c) => Some(json!({ "$and": [c, owner] })),
401    }
402}
403
404// ─── 字段级过滤(写) ─────────────────────────────────────────
405
406pub fn get_writable_fields(
407    rules: &RoleRules,
408    schema: &Schema,
409    ctx: Option<&Context>,
410) -> Option<HashSet<String>> {
411    let c = ctx?;
412    let schema_writable = can_write_schema(rules, schema, ctx);
413    let mut allowed = HashSet::new();
414    for (key, field) in &schema.fields {
415        match &field.write {
416            Some(wl) => {
417                if evaluate(rules, Some(c), Some(wl), Doc::Missing, schema.owner_key()) {
418                    allowed.insert(key.clone());
419                }
420            }
421            None => {
422                if schema_writable {
423                    allowed.insert(key.clone());
424                }
425            }
426        }
427    }
428    Some(allowed)
429}
430
431/// 过滤写入数据:只保留当前用户可写的字段(点号路径按 root 字段检查)
432pub fn filter_writable_data(
433    rules: &RoleRules,
434    schema: &Schema,
435    ctx: Option<&Context>,
436    data: &Value,
437) -> Value {
438    let Some(writable) = get_writable_fields(rules, schema, ctx) else {
439        return data.clone();
440    };
441    let Some(obj) = data.as_object() else {
442        return data.clone();
443    };
444    let mut result = Map::new();
445    for (key, val) in obj {
446        let root = key.split('.').next().unwrap_or(key);
447        // `_id` 是文档标识而非普通数据字段,豁免写权限过滤(D-03:否则
448        // 「显式提供 _id」路径在有权限过滤的上下文中永远不可达)
449        if key == "_id" || writable.contains(root) {
450            result.insert(key.clone(), val.clone());
451        }
452    }
453    Value::Object(result)
454}