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/// 评估当前用户是否满足指定角色白名单
71pub fn evaluate(ctx: Option<&Context>, role_list: Option<&[String]>, doc: Doc) -> bool {
72    let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
73    if empty {
74        // 无角色白名单 = schema/字段无权限配置 → 按角色取默认行为
75        return match ctx {
76            None => true,
77            Some(c) => {
78                if c.internal {
79                    true
80                } else {
81                    !c.roles
82                        .clone()
83                        .unwrap_or_default()
84                        .iter()
85                        .any(|r| r == "guest")
86                }
87            }
88        };
89    }
90
91    let Some(c) = ctx else { return true };
92    if c.internal {
93        return true;
94    }
95
96    let roles = c.roles.clone().unwrap_or_default();
97    // super_admin / admin 自动放行
98    if roles.iter().any(|r| r == "super_admin" || r == "admin") {
99        return true;
100    }
101
102    // ① 角色匹配
103    let effective = if roles.is_empty() {
104        vec![c.role.clone().unwrap_or_default()]
105    } else {
106        roles
107    };
108    // `empty` 已早退,此处 role_list 必为 Some 且非空
109    let Some(list) = role_list else { return true };
110    if list.iter().any(|r| effective.iter().any(|x| x == r)) {
111        return true;
112    }
113
114    // ② 创作者匹配
115    match_creator(c, doc, list)
116}
117
118fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
119    if !role_list.iter().any(|r| r == "creator") {
120        return false;
121    }
122    match doc {
123        Doc::Missing => true,
124        Doc::NoResult => false,
125        Doc::Doc(d) => {
126            let uid = ctx.user_id.clone().unwrap_or_default();
127            if uid.is_empty() {
128                return false;
129            }
130            d.get("createdBy")
131                .and_then(|v| v.as_str())
132                .map(|cb| cb == uid)
133                .unwrap_or(false)
134        }
135    }
136}
137
138// ─── 字段级过滤(读) ─────────────────────────────────────────
139
140/// ctx 为 None 时返回 None(= 不做裁剪)
141pub fn get_readable_fields(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
142    let c = ctx?;
143    let mut allowed = HashSet::new();
144    for (key, field) in &schema.fields {
145        match &field.read {
146            Some(rl) => {
147                if evaluate(Some(c), Some(rl), Doc::Missing) {
148                    allowed.insert(key.clone());
149                }
150            }
151            None => {
152                allowed.insert(key.clone());
153            }
154        }
155    }
156    Some(allowed)
157}
158
159pub fn get_readable_computes(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
160    let c = ctx?;
161    let mut allowed = HashSet::new();
162    for (key, comp) in &schema.computes {
163        match &comp.read {
164            Some(rl) => {
165                if evaluate(Some(c), Some(rl), Doc::Missing) {
166                    allowed.insert(key.clone());
167                }
168            }
169            None => {
170                allowed.insert(key.clone());
171            }
172        }
173    }
174    Some(allowed)
175}
176
177pub fn get_readable_relations(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
178    let c = ctx?;
179    let mut allowed = HashSet::new();
180    for (key, rel) in &schema.relations {
181        match &rel.read {
182            Some(rl) => {
183                if evaluate(Some(c), Some(rl), Doc::Missing) {
184                    allowed.insert(key.clone());
185                }
186            }
187            None => {
188                allowed.insert(key.clone());
189            }
190        }
191    }
192    Some(allowed)
193}
194
195/// 单个字段读权限判定(点号路径按 root 字段判定)。
196///
197/// - `ctx = None` → 放行(fail-open,与默认姿态一致);
198/// - 字段未在 schema `fields` 中声明 → 放行(无 `field.read` 配置可依);
199/// - 字段在 schema 中且配了 `read` 白名单 → 按 [`get_readable_fields`] 判定。
200pub fn is_field_readable(schema: &Schema, ctx: Option<&Context>, field: &str) -> bool {
201    let root = field.split('.').next().unwrap_or(field);
202    if !schema.fields.contains_key(root) {
203        return true;
204    }
205    match get_readable_fields(schema, ctx) {
206        None => true,
207        Some(allowed) => allowed.contains(root),
208    }
209}
210
211/// 单个关系读权限判定(R6/L1)。`ctx = None` → 放行(fail-open)。
212pub fn is_relation_readable(schema: &Schema, ctx: Option<&Context>, rel: &str) -> bool {
213    match get_readable_relations(schema, ctx) {
214        None => true,
215        Some(allowed) => allowed.contains(rel),
216    }
217}
218
219// ─── Schema 级检查 ────────────────────────────────────────────
220
221pub fn can_read_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
222    evaluate(ctx, schema.read.as_deref(), Doc::Missing)
223}
224
225/// 游客无论 schema.write 如何配置,均无写入权限
226pub fn can_write_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
227    if let Some(c) = ctx {
228        if has_role(c, "guest") {
229            return false;
230        }
231        // R2:write 显式配置为空白名单(`[]`)= 未声明任何可写角色 → 拒绝一切写,
232        // 但 super_admin/admin 仍保留编辑权;internal 一律放行。
233        if !c.internal && matches!(schema.write, Some(ref w) if w.is_empty()) {
234            let roles = c.roles.clone().unwrap_or_default();
235            if !roles.iter().any(|r| r == "super_admin" || r == "admin") {
236                return false;
237            }
238        }
239    }
240    evaluate(ctx, schema.write.as_deref(), Doc::Missing)
241}
242
243fn has_role(ctx: &Context, role: &str) -> bool {
244    ctx.roles
245        .as_ref()
246        .map(|r| r.iter().any(|x| x == role))
247        .unwrap_or(false)
248}
249
250// ─── 所有者条件注入 ──────────────────────────────────────────
251
252pub fn should_inject_owner_condition(schema: &Schema, ctx: Option<&Context>) -> bool {
253    let Some(c) = ctx else { return false };
254    // JS `!ctx.userId`:空串同样视为缺席
255    if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
256        return false;
257    }
258    if c.internal {
259        return false;
260    }
261    let roles = c.roles.clone().unwrap_or_default();
262    if roles.iter().any(|r| r == "super_admin" || r == "admin") {
263        return false;
264    }
265    let effective = if roles.is_empty() {
266        vec![c.role.clone().unwrap_or_default()]
267    } else {
268        roles
269    };
270    let read = schema.read.clone().unwrap_or_default();
271    let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
272    if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
273        return false;
274    }
275    read.iter().any(|r| r == "creator")
276}
277
278/// 非 admin 用户只看自己的数据:`creator` 专属读权限时注入 `createdBy` 条件
279pub fn merge_owner_condition(
280    schema: &Schema,
281    ctx: Option<&Context>,
282    condition: Option<Value>,
283) -> Option<Value> {
284    if !should_inject_owner_condition(schema, ctx) {
285        return condition;
286    }
287    let owner = json!({ "createdBy": ctx?.user_id });
288    match condition.filter(is_truthy) {
289        None => Some(owner),
290        Some(c) => Some(json!({ "$and": [c, owner] })),
291    }
292}
293
294// ─── 字段级过滤(写) ─────────────────────────────────────────
295
296pub fn get_writable_fields(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
297    let c = ctx?;
298    let schema_writable = can_write_schema(schema, ctx);
299    let mut allowed = HashSet::new();
300    for (key, field) in &schema.fields {
301        match &field.write {
302            Some(wl) => {
303                if evaluate(Some(c), Some(wl), Doc::Missing) {
304                    allowed.insert(key.clone());
305                }
306            }
307            None => {
308                if schema_writable {
309                    allowed.insert(key.clone());
310                }
311            }
312        }
313    }
314    Some(allowed)
315}
316
317/// 过滤写入数据:只保留当前用户可写的字段(点号路径按 root 字段检查)
318pub fn filter_writable_data(schema: &Schema, ctx: Option<&Context>, data: &Value) -> Value {
319    let Some(writable) = get_writable_fields(schema, ctx) else {
320        return data.clone();
321    };
322    let Some(obj) = data.as_object() else {
323        return data.clone();
324    };
325    let mut result = Map::new();
326    for (key, val) in obj {
327        let root = key.split('.').next().unwrap_or(key);
328        // `_id` 是文档标识而非普通数据字段,豁免写权限过滤(D-03:否则
329        // 「显式提供 _id」路径在有权限过滤的上下文中永远不可达)
330        if key == "_id" || writable.contains(root) {
331            result.insert(key.clone(), val.clone());
332        }
333    }
334    Value::Object(result)
335}