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 let v = grant_row_restriction(g, c)?;
472 conds.push(v);
473 }
474 match conds.len() {
475 0 => None,
476 1 => Some(conds.pop().unwrap_or(Value::Null)),
477 _ => Some(json!({ "$or": conds })),
478 }
479}
480
481pub fn merge_row_condition(
484 registry: &Registry,
485 schema: &Schema,
486 ctx: Option<&Context>,
487 action: &str,
488 base: Option<Value>,
489) -> Option<Value> {
490 let meaningful = |v: Option<Value>| -> Option<Value> {
491 v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
492 };
493 let rbac_cond = row_condition(registry, schema, ctx, action);
494 match (meaningful(base), rbac_cond) {
495 (None, r) => r,
496 (b, None) => b,
497 (Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
498 }
499}
500
501pub fn overlay_readable_fields(
504 registry: &Registry,
505 model: &str,
506 ctx: Option<&Context>,
507 base: Option<HashSet<String>>,
508) -> Option<HashSet<String>> {
509 let Some(p) = registry.rbac() else {
510 return base;
511 };
512 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
513 return base;
514 };
515 let Some(rf) = &d.read_fields else {
516 return base;
517 };
518 match base {
519 None => {
520 let mut s: HashSet<String> = rf.clone();
521 s.insert("_id".to_string());
522 Some(s)
523 }
524 Some(b) => {
525 let keep_id = b.contains("_id");
526 let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
527 if keep_id {
528 s.insert("_id".to_string());
529 }
530 Some(s)
531 }
532 }
533}
534
535pub fn overlay_writable_fields(
537 registry: &Registry,
538 model: &str,
539 ctx: Option<&Context>,
540 base: Option<HashSet<String>>,
541) -> Option<HashSet<String>> {
542 let Some(p) = registry.rbac() else {
543 return base;
544 };
545 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
546 return base;
547 };
548 let Some(wf) = &d.write_fields else {
549 return base;
550 };
551 match base {
552 None => {
553 let mut s: HashSet<String> = wf.clone();
554 s.insert("_id".to_string());
555 Some(s)
556 }
557 Some(b) => {
558 let keep_id = b.contains("_id");
559 let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
560 if keep_id {
561 s.insert("_id".to_string());
562 }
563 Some(s)
564 }
565 }
566}
567
568pub fn filter_writable_data_overlay(
573 registry: &Registry,
574 schema: &Schema,
575 ctx: Option<&Context>,
576 data: &Value,
577) -> Value {
578 if registry.rbac().is_none() {
579 return crate::permission::filter_writable_data(registry.role_rules(), schema, ctx, data);
580 }
581 let base = get_writable_fields(registry.role_rules(), schema, ctx);
582 let writable = overlay_writable_fields(registry, &schema.name, ctx, base);
583 let Some(writable) = writable else {
584 return data.clone();
585 };
586 let Some(obj) = data.as_object() else {
587 return data.clone();
588 };
589 let mut result = Map::new();
590 for (key, val) in obj {
591 let root = key.split('.').next().unwrap_or(key);
592 if key == "_id" || writable.contains(root) {
593 result.insert(key.clone(), val.clone());
594 }
595 }
596 Value::Object(result)
597}
598
599pub fn is_field_readable_overlay(
602 registry: &Registry,
603 schema: &Schema,
604 ctx: Option<&Context>,
605 field: &str,
606) -> bool {
607 if !crate::permission::is_field_readable(registry.role_rules(), schema, ctx, field) {
608 return false;
609 }
610 let Some(p) = registry.rbac() else {
611 return true;
612 };
613 let Some(c) = ctx else { return true };
614 if c.internal {
615 return true;
616 }
617 let Some(d) = decide(
618 p,
619 ®istry.role_rules().exempt_roles,
620 Some(c),
621 &schema.name,
622 ) else {
623 return true;
624 };
625 match &d.read_fields {
626 None => true,
627 Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
628 }
629}
630
631pub fn probe_condition_fields(
635 registry: &Registry,
636 schema: &Schema,
637 ctx: Option<&Context>,
638 action: &str,
639) -> Vec<String> {
640 let Some(p) = registry.rbac() else {
641 return Vec::new();
642 };
643 let Some(c) = ctx else { return Vec::new() };
644 if c.internal {
645 return Vec::new();
646 }
647 if decide(
648 p,
649 ®istry.role_rules().exempt_roles,
650 Some(c),
651 &schema.name,
652 )
653 .is_none()
654 {
655 return Vec::new();
656 }
657 let roles = effective_roles(c);
658 let mut fields = Vec::new();
659 for g in p.grants.iter().filter(|g| {
660 g.actions.iter().any(|a| a == action)
661 && (g.model == schema.name || g.model == "*")
662 && roles.iter().any(|r| r == &g.role)
663 }) {
664 if let Some(cond) = &g.condition {
665 if let Some(obj) = cond.as_object() {
666 for k in obj.keys() {
667 if !fields.iter().any(|x: &String| x == k) {
668 fields.push(k.clone());
669 }
670 }
671 }
672 }
673 }
674 fields
675}
676
677fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
683 if !g.owner_only && g.condition.is_none() {
684 return None;
685 }
686 let owner = if g.owner_only {
687 let uid = c.user_id.clone().unwrap_or_default();
688 if uid.is_empty() {
689 Some(json!({ "__rbac_impossible__": true }))
691 } else {
692 Some(json!({ "createdBy": uid }))
693 }
694 } else {
695 None
696 };
697 match (owner, g.condition.clone()) {
698 (None, cond) => cond,
699 (owner, None) => owner,
700 (Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
701 }
702}
703
704fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
706 let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
707 return false;
708 };
709 cond_map
710 .iter()
711 .all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
712}