1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum RbacMode {
31 #[default]
32 Overlay,
33 Enforce,
34}
35
36#[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
54pub 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, actions: Vec<String>, read_fields: Option<HashSet<String>>,
72 write_fields: Option<HashSet<String>>,
73 owner_only: bool,
74 condition: Option<Value>, }
76
77#[derive(Debug, Clone, Default)]
78pub struct RbacPolicy {
79 mode: RbacMode,
80 managed_roles: HashSet<String>,
81 grants: Vec<Grant>,
82}
83
84#[derive(Debug, Clone)]
86pub struct ModelDecision {
87 pub allowed_actions: HashSet<String>, pub read_fields: Option<HashSet<String>>,
90 pub write_fields: Option<HashSet<String>>,
91}
92
93pub fn deny_msg(action: &str, model: &str) -> String {
95 format!("ERR_PERMISSION:RBAC:角色对 [{model}] 无 [{action}] 权限")
96}
97
98impl RbacPolicy {
101 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 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
254fn 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
264pub 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 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
327pub 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
343pub 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
358pub 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
399pub 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, 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
433pub 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
453pub 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
481pub 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
508pub 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
539pub 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
562pub 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
599fn 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 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
626fn 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}