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) -> bool {
151    let Some(c) = ctx else { return true };
152    if c.internal {
153        return true;
154    }
155    // 豁免清单(默认空——无隐形豁免;清单化语义见设计 §11.3)
156    if has_any(c, &rules.exempt_roles) {
157        return true;
158    }
159    let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
160    if empty {
161        // 未配置姿态显式化(原 empty 分支的 guest 读拒随清单化移除)
162        return rules.unconfigured == UnconfiguredPolicy::Open;
163    }
164
165    // ① 角色匹配
166    let effective = effective(c);
167    // `empty` 已早退,此处 role_list 必为 Some 且非空
168    let Some(list) = role_list else { return true };
169    if list.iter().any(|r| effective.iter().any(|x| x == r)) {
170        return true;
171    }
172
173    // ② 创作者匹配
174    match_creator(c, doc, list)
175}
176
177fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
178    if !role_list.iter().any(|r| r == "creator") {
179        return false;
180    }
181    match doc {
182        Doc::Missing => true,
183        Doc::NoResult => false,
184        Doc::Doc(d) => {
185            let uid = ctx.user_id.clone().unwrap_or_default();
186            if uid.is_empty() {
187                return false;
188            }
189            d.get("createdBy")
190                .and_then(|v| v.as_str())
191                .map(|cb| cb == uid)
192                .unwrap_or(false)
193        }
194    }
195}
196
197// ─── 字段级过滤(读) ─────────────────────────────────────────
198
199/// ctx 为 None 时返回 None(= 不做裁剪)
200pub fn get_readable_fields(
201    rules: &RoleRules,
202    schema: &Schema,
203    ctx: Option<&Context>,
204) -> Option<HashSet<String>> {
205    let c = ctx?;
206    let mut allowed = HashSet::new();
207    for (key, field) in &schema.fields {
208        match &field.read {
209            Some(rl) => {
210                if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
211                    allowed.insert(key.clone());
212                }
213            }
214            None => {
215                allowed.insert(key.clone());
216            }
217        }
218    }
219    Some(allowed)
220}
221
222pub fn get_readable_computes(
223    rules: &RoleRules,
224    schema: &Schema,
225    ctx: Option<&Context>,
226) -> Option<HashSet<String>> {
227    let c = ctx?;
228    let mut allowed = HashSet::new();
229    for (key, comp) in &schema.computes {
230        match &comp.read {
231            Some(rl) => {
232                if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
233                    allowed.insert(key.clone());
234                }
235            }
236            None => {
237                allowed.insert(key.clone());
238            }
239        }
240    }
241    Some(allowed)
242}
243
244pub fn get_readable_relations(
245    rules: &RoleRules,
246    schema: &Schema,
247    ctx: Option<&Context>,
248) -> Option<HashSet<String>> {
249    let c = ctx?;
250    let mut allowed = HashSet::new();
251    for (key, rel) in &schema.relations {
252        match &rel.read {
253            Some(rl) => {
254                if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
255                    allowed.insert(key.clone());
256                }
257            }
258            None => {
259                allowed.insert(key.clone());
260            }
261        }
262    }
263    Some(allowed)
264}
265
266/// 单个字段读权限判定(点号路径按 root 字段判定)。
267///
268/// - `ctx = None` → 放行(fail-open,与默认姿态一致);
269/// - 字段未在 schema `fields` 中声明 → 放行(无 `field.read` 配置可依);
270/// - 字段在 schema 中且配了 `read` 白名单 → 按 [`get_readable_fields`] 判定。
271pub fn is_field_readable(
272    rules: &RoleRules,
273    schema: &Schema,
274    ctx: Option<&Context>,
275    field: &str,
276) -> bool {
277    let root = field.split('.').next().unwrap_or(field);
278    if !schema.fields.contains_key(root) {
279        return true;
280    }
281    match get_readable_fields(rules, schema, ctx) {
282        None => true,
283        Some(allowed) => allowed.contains(root),
284    }
285}
286
287/// 单个关系读权限判定(R6/L1)。`ctx = None` → 放行(fail-open)。
288pub fn is_relation_readable(
289    rules: &RoleRules,
290    schema: &Schema,
291    ctx: Option<&Context>,
292    rel: &str,
293) -> bool {
294    match get_readable_relations(rules, schema, ctx) {
295        None => true,
296        Some(allowed) => allowed.contains(rel),
297    }
298}
299
300// ─── Schema 级检查 ────────────────────────────────────────────
301
302pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
303    evaluate(rules, ctx, schema.read.as_deref(), Doc::Missing)
304}
305
306/// 拒写清单命中者一切写路径拒绝(读不受影响,默认空——无拒写);
307/// R2 语义保留:write 显式配置为空白名单(`[]`)= 未声明任何可写角色 → 拒绝一切写,
308/// 豁免清单命中者与 internal 例外。
309pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
310    if let Some(c) = ctx {
311        if has_any(c, &rules.deny_write_roles) {
312            return false;
313        }
314        if !c.internal
315            && matches!(schema.write, Some(ref w) if w.is_empty())
316            && !has_any(c, &rules.exempt_roles)
317        {
318            return false;
319        }
320    }
321    evaluate(rules, ctx, schema.write.as_deref(), Doc::Missing)
322}
323
324/// 有效角色集:`roles` 非空用之,否则回落单 `role`——对齐 `rbac::effective_roles`
325/// (rbac.rs)与白名单匹配语义。清单命中一律用本函数(不再存在
326/// 「roles 为空则清单失明」的隐形差异)。
327fn effective(c: &Context) -> Vec<String> {
328    match &c.roles {
329        Some(r) if !r.is_empty() => r.clone(),
330        _ => vec![c.role.clone().unwrap_or_default()],
331    }
332}
333
334/// 有效角色集与清单是否有交集
335fn has_any(c: &Context, list: &[String]) -> bool {
336    effective(c).iter().any(|r| list.iter().any(|x| x == r))
337}
338
339/// [`has_any`] 的 crate 内公开包装:command 层写路径复用,避免各调用点手写 effective 归一
340pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
341    has_any(c, list)
342}
343
344// ─── 所有者条件注入 ──────────────────────────────────────────
345
346pub fn should_inject_owner_condition(
347    rules: &RoleRules,
348    schema: &Schema,
349    ctx: Option<&Context>,
350) -> bool {
351    let Some(c) = ctx else { return false };
352    // JS `!ctx.userId`:空串同样视为缺席
353    if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
354        return false;
355    }
356    if c.internal {
357        return false;
358    }
359    if has_any(c, &rules.exempt_roles) {
360        return false;
361    }
362    let effective = effective(c);
363    let read = schema.read.clone().unwrap_or_default();
364    let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
365    if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
366        return false;
367    }
368    read.iter().any(|r| r == "creator")
369}
370
371/// 非 admin 用户只看自己的数据:`creator` 专属读权限时注入 `createdBy` 条件
372pub fn merge_owner_condition(
373    rules: &RoleRules,
374    schema: &Schema,
375    ctx: Option<&Context>,
376    condition: Option<Value>,
377) -> Option<Value> {
378    if !should_inject_owner_condition(rules, schema, ctx) {
379        return condition;
380    }
381    let owner = json!({ "createdBy": ctx?.user_id });
382    match condition.filter(is_truthy) {
383        None => Some(owner),
384        Some(c) => Some(json!({ "$and": [c, owner] })),
385    }
386}
387
388// ─── 字段级过滤(写) ─────────────────────────────────────────
389
390pub fn get_writable_fields(
391    rules: &RoleRules,
392    schema: &Schema,
393    ctx: Option<&Context>,
394) -> Option<HashSet<String>> {
395    let c = ctx?;
396    let schema_writable = can_write_schema(rules, schema, ctx);
397    let mut allowed = HashSet::new();
398    for (key, field) in &schema.fields {
399        match &field.write {
400            Some(wl) => {
401                if evaluate(rules, Some(c), Some(wl), Doc::Missing) {
402                    allowed.insert(key.clone());
403                }
404            }
405            None => {
406                if schema_writable {
407                    allowed.insert(key.clone());
408                }
409            }
410        }
411    }
412    Some(allowed)
413}
414
415/// 过滤写入数据:只保留当前用户可写的字段(点号路径按 root 字段检查)
416pub fn filter_writable_data(
417    rules: &RoleRules,
418    schema: &Schema,
419    ctx: Option<&Context>,
420    data: &Value,
421) -> Value {
422    let Some(writable) = get_writable_fields(rules, schema, ctx) else {
423        return data.clone();
424    };
425    let Some(obj) = data.as_object() else {
426        return data.clone();
427    };
428    let mut result = Map::new();
429    for (key, val) in obj {
430        let root = key.split('.').next().unwrap_or(key);
431        // `_id` 是文档标识而非普通数据字段,豁免写权限过滤(D-03:否则
432        // 「显式提供 _id」路径在有权限过滤的上下文中永远不可达)
433        if key == "_id" || writable.contains(root) {
434            result.insert(key.clone(), val.clone());
435        }
436    }
437    Value::Object(result)
438}