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