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(
133 "RBAC mode=enforce 时 roles 不能为空(无受管角色 = 策略空转,属配置错误)"
134 .to_string(),
135 );
136 }
137
138 let mut grants = Vec::new();
139 match obj.get("grants") {
140 None | Some(Value::Null) => {}
141 Some(Value::Array(gs)) => {
142 for (i, g) in gs.iter().enumerate() {
143 grants.push(parse_grant(i, g)?);
144 }
145 }
146 Some(other) => return Err(format!("RBAC grants 必须是数组,实际 {other}")),
147 }
148
149 Ok(Self {
150 mode,
151 managed_roles,
152 grants,
153 })
154 }
155}
156
157fn parse_grant(idx: usize, v: &Value) -> Result<Grant, String> {
158 let o = v
159 .as_object()
160 .ok_or_else(|| format!("RBAC grants[{idx}] 必须是对象"))?;
161 let get_str = |k: &str| -> Result<String, String> {
162 o.get(k)
163 .and_then(|x| x.as_str())
164 .filter(|s| !s.is_empty())
165 .map(String::from)
166 .ok_or_else(|| format!("RBAC grants[{idx}].{k} 必须是非空字符串"))
167 };
168 let role = get_str("role")?;
169 let model = get_str("model")?;
170
171 let actions_raw = o
172 .get("actions")
173 .and_then(|x| x.as_array())
174 .ok_or_else(|| format!("RBAC grants[{idx}].actions 必须是非空数组"))?;
175 if actions_raw.is_empty() {
176 return Err(format!(
177 "RBAC grants[{idx}].actions 不能为空(不授权请移除该 grant)"
178 ));
179 }
180 let mut actions: Vec<String> = Vec::new();
181 for a in actions_raw {
182 let s = a
183 .as_str()
184 .ok_or_else(|| format!("RBAC grants[{idx}].actions 含非字符串项 {a}"))?;
185 let expanded: &[&str] = match s {
186 "read" => &["read"],
187 "insert" => &["insert"],
188 "update" => &["update"],
189 "remove" => &["remove"],
190 "write" => &["insert", "update", "remove"],
191 other => {
192 return Err(format!(
193 "RBAC grants[{idx}].actions 含非法值 \"{other}\"(仅支持 read/insert/update/remove/write)"
194 ))
195 }
196 };
197 for e in expanded {
198 if !actions.iter().any(|x| x == e) {
199 actions.push((*e).to_string());
200 }
201 }
202 }
203
204 let parse_fields = |k: &str| -> Result<Option<HashSet<String>>, String> {
205 match o.get(k) {
206 None | Some(Value::Null) => Ok(None),
207 Some(Value::Array(fs)) => {
208 if fs.is_empty() {
209 return Err(format!(
210 "RBAC grants[{idx}].{k} 不能为空数组(全禁请移除该 grant)"
211 ));
212 }
213 let mut set = HashSet::new();
214 for f in fs {
215 let s = f.as_str().filter(|s| !s.is_empty()).ok_or_else(|| {
216 format!("RBAC grants[{idx}].{k} 含非法项 {f}(须非空字符串)")
217 })?;
218 set.insert(s.to_string());
219 }
220 Ok(Some(set))
221 }
222 Some(other) => Err(format!(
223 "RBAC grants[{idx}].{k} 必须是字符串数组,实际 {other}"
224 )),
225 }
226 };
227 let read_fields = parse_fields("readFields")?;
228 let write_fields = parse_fields("writeFields")?;
229
230 let owner_only = o.get("ownerOnly").map(is_truthy).unwrap_or(false);
231
232 let condition = match o.get("condition") {
235 None | Some(Value::Null) => None,
236 Some(cm @ Value::Object(_)) => {
237 let entries = cm.as_object().expect("已按 Object 匹配,此处必为对象");
238 for (k, val) in entries {
239 if k.is_empty() || k.starts_with('$') {
240 return Err(format!(
241 "RBAC grants[{idx}].condition 键 \"{k}\" 非法(第一批仅支持等值字段条件,键不得以 $ 开头)"
242 ));
243 }
244 if !val.is_boolean() && !val.is_number() && !val.is_string() && !val.is_null() {
245 return Err(format!(
246 "RBAC grants[{idx}].condition[\"{k}\"] 必须是标量(string/number/bool/null),实际 {val}"
247 ));
248 }
249 }
250 Some(cm.clone())
251 }
252 Some(other) => {
253 return Err(format!(
254 "RBAC grants[{idx}].condition 必须是对象(等值条件),实际 {other}"
255 ))
256 }
257 };
258
259 Ok(Grant {
260 role,
261 model,
262 actions,
263 read_fields,
264 write_fields,
265 owner_only,
266 condition,
267 })
268}
269
270fn effective_roles(c: &Context) -> Vec<String> {
274 match &c.roles {
275 Some(r) if !r.is_empty() => r.clone(),
276 _ => vec![c.role.clone().unwrap_or_default()],
277 }
278}
279
280pub fn decide(
284 policy: &RbacPolicy,
285 exempt_roles: &[String],
286 ctx: Option<&Context>,
287 model: &str,
288) -> Option<ModelDecision> {
289 let c = ctx?;
290 if c.internal {
291 return None;
292 }
293 let roles = effective_roles(c);
294 if roles.iter().any(|r| exempt_roles.iter().any(|x| x == r)) {
295 return None;
296 }
297
298 let matched: Vec<&Grant> = policy
299 .grants
300 .iter()
301 .filter(|g| roles.iter().any(|r| r == &g.role) && (g.model == model || g.model == "*"))
302 .collect();
303
304 match policy.mode {
305 RbacMode::Overlay if matched.is_empty() => return None,
306 RbacMode::Enforce => {
307 let managed = roles.iter().any(|r| policy.managed_roles.contains(r));
308 if !managed {
309 return None;
310 }
311 }
312 _ => {}
313 }
314
315 let mut allowed_actions = HashSet::new();
318 let mut read_all = false;
319 let mut read_union: HashSet<String> = HashSet::new();
320 let mut write_all = false;
321 let mut write_union: HashSet<String> = HashSet::new();
322 for g in &matched {
323 for a in &g.actions {
324 allowed_actions.insert(a.clone());
325 }
326 if g.actions.iter().any(|a| a == "read") {
327 match &g.read_fields {
328 None => read_all = true,
329 Some(s) => read_union.extend(s.iter().cloned()),
330 }
331 }
332 if g.actions
333 .iter()
334 .any(|a| a == "insert" || a == "update" || a == "remove")
335 {
336 match &g.write_fields {
337 None => write_all = true,
338 Some(s) => write_union.extend(s.iter().cloned()),
339 }
340 }
341 }
342 let read_fields = if read_all || read_union.is_empty() {
343 None
344 } else {
345 Some(read_union)
346 };
347 let write_fields = if write_all || write_union.is_empty() {
348 None
349 } else {
350 Some(write_union)
351 };
352 Some(ModelDecision {
353 allowed_actions,
354 read_fields,
355 write_fields,
356 })
357}
358
359pub fn ensure_read(
363 registry: &Registry,
364 schema: &Schema,
365 ctx: Option<&Context>,
366) -> Result<(), String> {
367 let Some(p) = registry.rbac() else {
368 return Ok(());
369 };
370 match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
371 None => Ok(()),
372 Some(d) if d.allowed_actions.contains("read") => Ok(()),
373 Some(_) => Err(deny_msg("read", &schema.name)),
374 }
375}
376
377pub fn ensure_write(
379 registry: &Registry,
380 schema: &Schema,
381 ctx: Option<&Context>,
382 action: WriteAction,
383) -> Result<(), String> {
384 let Some(p) = registry.rbac() else {
385 return Ok(());
386 };
387 match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
388 None => Ok(()),
389 Some(d) if d.allowed_actions.contains(action.as_str()) => Ok(()),
390 Some(_) => Err(deny_msg(action.as_str(), &schema.name)),
391 }
392}
393
394pub fn ensure_write_on_doc(
399 registry: &Registry,
400 schema: &Schema,
401 ctx: Option<&Context>,
402 action: WriteAction,
403 doc: &Value,
404) -> Result<(), String> {
405 let Some(p) = registry.rbac() else {
406 return Ok(());
407 };
408 let Some(c) = ctx else { return Ok(()) };
409 if c.internal {
410 return Ok(());
411 }
412 let Some(d) = decide(
413 p,
414 ®istry.role_rules().exempt_roles,
415 Some(c),
416 &schema.name,
417 ) else {
418 return Ok(());
419 };
420 if !d.allowed_actions.contains(action.as_str()) {
421 return Err(deny_msg(action.as_str(), &schema.name));
422 }
423 let roles = effective_roles(c);
424 let hit = p.grants.iter().any(|g| {
425 g.actions.iter().any(|a| a == action.as_str())
426 && (g.model == schema.name || g.model == "*")
427 && roles.iter().any(|r| r == &g.role)
428 && match grant_row_restriction(g, c) {
429 None => true,
430 Some(cond) => doc_matches_eq(doc, &cond),
431 }
432 });
433 if hit {
434 Ok(())
435 } else {
436 Err(format!(
437 "ERR_PERMISSION:RBAC:角色对 [{}] 的 [{}] 限于本人/条件内文档",
438 schema.name,
439 action.as_str()
440 ))
441 }
442}
443
444pub fn row_condition(
448 registry: &Registry,
449 schema: &Schema,
450 ctx: Option<&Context>,
451 action: &str,
452) -> Option<Value> {
453 let p = registry.rbac()?;
454 let c = ctx?;
455 if c.internal {
456 return None;
457 }
458 decide(
459 p,
460 ®istry.role_rules().exempt_roles,
461 Some(c),
462 &schema.name,
463 )?;
464 let roles = effective_roles(c);
465 let mut conds: Vec<Value> = Vec::new();
466 for g in p.grants.iter().filter(|g| {
467 g.actions.iter().any(|a| a == action)
468 && (g.model == schema.name || g.model == "*")
469 && roles.iter().any(|r| r == &g.role)
470 }) {
471 match grant_row_restriction(g, c) {
472 None => return None, Some(v) => conds.push(v),
474 }
475 }
476 match conds.len() {
477 0 => None,
478 1 => Some(conds.pop().unwrap_or(Value::Null)),
479 _ => Some(json!({ "$or": conds })),
480 }
481}
482
483pub fn merge_row_condition(
486 registry: &Registry,
487 schema: &Schema,
488 ctx: Option<&Context>,
489 action: &str,
490 base: Option<Value>,
491) -> Option<Value> {
492 let meaningful = |v: Option<Value>| -> Option<Value> {
493 v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
494 };
495 let rbac_cond = row_condition(registry, schema, ctx, action);
496 match (meaningful(base), rbac_cond) {
497 (None, r) => r,
498 (b, None) => b,
499 (Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
500 }
501}
502
503pub fn overlay_readable_fields(
506 registry: &Registry,
507 model: &str,
508 ctx: Option<&Context>,
509 base: Option<HashSet<String>>,
510) -> Option<HashSet<String>> {
511 let Some(p) = registry.rbac() else {
512 return base;
513 };
514 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
515 return base;
516 };
517 let Some(rf) = &d.read_fields else {
518 return base;
519 };
520 match base {
521 None => {
522 let mut s: HashSet<String> = rf.clone();
523 s.insert("_id".to_string());
524 Some(s)
525 }
526 Some(b) => {
527 let keep_id = b.contains("_id");
528 let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
529 if keep_id {
530 s.insert("_id".to_string());
531 }
532 Some(s)
533 }
534 }
535}
536
537pub fn overlay_writable_fields(
539 registry: &Registry,
540 model: &str,
541 ctx: Option<&Context>,
542 base: Option<HashSet<String>>,
543) -> Option<HashSet<String>> {
544 let Some(p) = registry.rbac() else {
545 return base;
546 };
547 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
548 return base;
549 };
550 let Some(wf) = &d.write_fields else {
551 return base;
552 };
553 match base {
554 None => {
555 let mut s: HashSet<String> = wf.clone();
556 s.insert("_id".to_string());
557 Some(s)
558 }
559 Some(b) => {
560 let keep_id = b.contains("_id");
561 let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
562 if keep_id {
563 s.insert("_id".to_string());
564 }
565 Some(s)
566 }
567 }
568}
569
570pub fn filter_writable_data_overlay(
575 registry: &Registry,
576 schema: &Schema,
577 ctx: Option<&Context>,
578 data: &Value,
579) -> Value {
580 if registry.rbac().is_none() {
581 return crate::permission::filter_writable_data(registry.role_rules(), schema, ctx, data);
582 }
583 let base = get_writable_fields(registry.role_rules(), schema, ctx);
584 let writable = overlay_writable_fields(registry, &schema.name, ctx, base);
585 let Some(writable) = writable else {
586 return data.clone();
587 };
588 let Some(obj) = data.as_object() else {
589 return data.clone();
590 };
591 let mut result = Map::new();
592 for (key, val) in obj {
593 let root = key.split('.').next().unwrap_or(key);
594 if key == "_id" || writable.contains(root) {
595 result.insert(key.clone(), val.clone());
596 }
597 }
598 Value::Object(result)
599}
600
601pub fn is_field_readable_overlay(
604 registry: &Registry,
605 schema: &Schema,
606 ctx: Option<&Context>,
607 field: &str,
608) -> bool {
609 if !crate::permission::is_field_readable(registry.role_rules(), schema, ctx, field) {
610 return false;
611 }
612 let Some(p) = registry.rbac() else {
613 return true;
614 };
615 let Some(c) = ctx else { return true };
616 if c.internal {
617 return true;
618 }
619 let Some(d) = decide(
620 p,
621 ®istry.role_rules().exempt_roles,
622 Some(c),
623 &schema.name,
624 ) else {
625 return true;
626 };
627 match &d.read_fields {
628 None => true,
629 Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
630 }
631}
632
633pub fn probe_condition_fields(
637 registry: &Registry,
638 schema: &Schema,
639 ctx: Option<&Context>,
640 action: &str,
641) -> Vec<String> {
642 let Some(p) = registry.rbac() else {
643 return Vec::new();
644 };
645 let Some(c) = ctx else { return Vec::new() };
646 if c.internal {
647 return Vec::new();
648 }
649 if decide(
650 p,
651 ®istry.role_rules().exempt_roles,
652 Some(c),
653 &schema.name,
654 )
655 .is_none()
656 {
657 return Vec::new();
658 }
659 let roles = effective_roles(c);
660 let mut fields = Vec::new();
661 for g in p.grants.iter().filter(|g| {
662 g.actions.iter().any(|a| a == action)
663 && (g.model == schema.name || g.model == "*")
664 && roles.iter().any(|r| r == &g.role)
665 }) {
666 if let Some(cond) = &g.condition {
667 if let Some(obj) = cond.as_object() {
668 for k in obj.keys() {
669 if !fields.iter().any(|x: &String| x == k) {
670 fields.push(k.clone());
671 }
672 }
673 }
674 }
675 }
676 fields
677}
678
679fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
685 if !g.owner_only && g.condition.is_none() {
686 return None;
687 }
688 let owner = if g.owner_only {
689 let uid = c.user_id.clone().unwrap_or_default();
690 if uid.is_empty() {
691 Some(json!({ "__rbac_impossible__": true }))
693 } else {
694 Some(json!({ "createdBy": uid }))
695 }
696 } else {
697 None
698 };
699 match (owner, g.condition.clone()) {
700 (None, cond) => cond,
701 (owner, None) => owner,
702 (Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
703 }
704}
705
706fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
708 let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
709 return false;
710 };
711 cond_map
712 .iter()
713 .all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
714}