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(
268 policy: &RbacPolicy,
269 exempt_roles: &[String],
270 ctx: Option<&Context>,
271 model: &str,
272) -> Option<ModelDecision> {
273 let c = ctx?;
274 if c.internal {
275 return None;
276 }
277 let roles = effective_roles(c);
278 if roles.iter().any(|r| exempt_roles.iter().any(|x| x == r)) {
279 return None;
280 }
281
282 let matched: Vec<&Grant> = policy
283 .grants
284 .iter()
285 .filter(|g| roles.iter().any(|r| r == &g.role) && (g.model == model || g.model == "*"))
286 .collect();
287
288 match policy.mode {
289 RbacMode::Overlay if matched.is_empty() => return None,
290 RbacMode::Enforce => {
291 let managed = roles.iter().any(|r| policy.managed_roles.contains(r));
292 if !managed {
293 return None;
294 }
295 }
296 _ => {}
297 }
298
299 let mut allowed_actions = HashSet::new();
302 let mut read_all = false;
303 let mut read_union: HashSet<String> = HashSet::new();
304 let mut write_all = false;
305 let mut write_union: HashSet<String> = HashSet::new();
306 for g in &matched {
307 for a in &g.actions {
308 allowed_actions.insert(a.clone());
309 }
310 if g.actions.iter().any(|a| a == "read") {
311 match &g.read_fields {
312 None => read_all = true,
313 Some(s) => read_union.extend(s.iter().cloned()),
314 }
315 }
316 if g
317 .actions
318 .iter()
319 .any(|a| a == "insert" || a == "update" || a == "remove")
320 {
321 match &g.write_fields {
322 None => write_all = true,
323 Some(s) => write_union.extend(s.iter().cloned()),
324 }
325 }
326 }
327 let read_fields = if read_all || read_union.is_empty() { None } else { Some(read_union) };
328 let write_fields = if write_all || write_union.is_empty() { None } else { Some(write_union) };
329 Some(ModelDecision { allowed_actions, read_fields, write_fields })
330}
331
332pub fn ensure_read(
336 registry: &Registry,
337 schema: &Schema,
338 ctx: Option<&Context>,
339) -> Result<(), String> {
340 let Some(p) = registry.rbac() else { return Ok(()) };
341 match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
342 None => Ok(()),
343 Some(d) if d.allowed_actions.contains("read") => Ok(()),
344 Some(_) => Err(deny_msg("read", &schema.name)),
345 }
346}
347
348pub fn ensure_write(
350 registry: &Registry,
351 schema: &Schema,
352 ctx: Option<&Context>,
353 action: WriteAction,
354) -> Result<(), String> {
355 let Some(p) = registry.rbac() else { return Ok(()) };
356 match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
357 None => Ok(()),
358 Some(d) if d.allowed_actions.contains(action.as_str()) => Ok(()),
359 Some(_) => Err(deny_msg(action.as_str(), &schema.name)),
360 }
361}
362
363pub fn ensure_write_on_doc(
368 registry: &Registry,
369 schema: &Schema,
370 ctx: Option<&Context>,
371 action: WriteAction,
372 doc: &Value,
373) -> Result<(), String> {
374 let Some(p) = registry.rbac() else { return Ok(()) };
375 let Some(c) = ctx else { return Ok(()) };
376 if c.internal {
377 return Ok(());
378 }
379 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, Some(c), &schema.name) else { return Ok(()) };
380 if !d.allowed_actions.contains(action.as_str()) {
381 return Err(deny_msg(action.as_str(), &schema.name));
382 }
383 let roles = effective_roles(c);
384 let hit = p.grants.iter().any(|g| {
385 g.actions.iter().any(|a| a == action.as_str())
386 && (g.model == schema.name || g.model == "*")
387 && roles.iter().any(|r| r == &g.role)
388 && match grant_row_restriction(g, c) {
389 None => true,
390 Some(cond) => doc_matches_eq(doc, &cond),
391 }
392 });
393 if hit {
394 Ok(())
395 } else {
396 Err(format!(
397 "ERR_PERMISSION:RBAC:角色对 [{}] 的 [{}] 限于本人/条件内文档",
398 schema.name,
399 action.as_str()
400 ))
401 }
402}
403
404pub fn row_condition(
408 registry: &Registry,
409 schema: &Schema,
410 ctx: Option<&Context>,
411 action: &str,
412) -> Option<Value> {
413 let p = registry.rbac()?;
414 let c = ctx?;
415 if c.internal {
416 return None;
417 }
418 decide(p, ®istry.role_rules().exempt_roles, Some(c), &schema.name)?;
419 let roles = effective_roles(c);
420 let mut conds: Vec<Value> = Vec::new();
421 for g in p.grants.iter().filter(|g| {
422 g.actions.iter().any(|a| a == action)
423 && (g.model == schema.name || g.model == "*")
424 && roles.iter().any(|r| r == &g.role)
425 }) {
426 match grant_row_restriction(g, c) {
427 None => return None, Some(v) => conds.push(v),
429 }
430 }
431 match conds.len() {
432 0 => None,
433 1 => Some(conds.pop().unwrap_or(Value::Null)),
434 _ => Some(json!({ "$or": conds })),
435 }
436}
437
438pub fn merge_row_condition(
441 registry: &Registry,
442 schema: &Schema,
443 ctx: Option<&Context>,
444 action: &str,
445 base: Option<Value>,
446) -> Option<Value> {
447 let meaningful = |v: Option<Value>| -> Option<Value> {
448 v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
449 };
450 let rbac_cond = row_condition(registry, schema, ctx, action);
451 match (meaningful(base), rbac_cond) {
452 (None, r) => r,
453 (b, None) => b,
454 (Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
455 }
456}
457
458pub fn overlay_readable_fields(
461 registry: &Registry,
462 model: &str,
463 ctx: Option<&Context>,
464 base: Option<HashSet<String>>,
465) -> Option<HashSet<String>> {
466 let Some(p) = registry.rbac() else { return base };
467 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else { return base };
468 let Some(rf) = &d.read_fields else { return base };
469 match base {
470 None => {
471 let mut s: HashSet<String> = rf.clone();
472 s.insert("_id".to_string());
473 Some(s)
474 }
475 Some(b) => {
476 let keep_id = b.contains("_id");
477 let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
478 if keep_id {
479 s.insert("_id".to_string());
480 }
481 Some(s)
482 }
483 }
484}
485
486pub fn overlay_writable_fields(
488 registry: &Registry,
489 model: &str,
490 ctx: Option<&Context>,
491 base: Option<HashSet<String>>,
492) -> Option<HashSet<String>> {
493 let Some(p) = registry.rbac() else { return base };
494 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else { return base };
495 let Some(wf) = &d.write_fields else { return base };
496 match base {
497 None => {
498 let mut s: HashSet<String> = wf.clone();
499 s.insert("_id".to_string());
500 Some(s)
501 }
502 Some(b) => {
503 let keep_id = b.contains("_id");
504 let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
505 if keep_id {
506 s.insert("_id".to_string());
507 }
508 Some(s)
509 }
510 }
511}
512
513pub fn filter_writable_data_overlay(
518 registry: &Registry,
519 schema: &Schema,
520 ctx: Option<&Context>,
521 data: &Value,
522) -> Value {
523 if registry.rbac().is_none() {
524 return crate::permission::filter_writable_data(registry.role_rules(), schema, ctx, data);
525 }
526 let base = get_writable_fields(registry.role_rules(), schema, ctx);
527 let writable = overlay_writable_fields(registry, &schema.name, ctx, base);
528 let Some(writable) = writable else {
529 return data.clone();
530 };
531 let Some(obj) = data.as_object() else {
532 return data.clone();
533 };
534 let mut result = Map::new();
535 for (key, val) in obj {
536 let root = key.split('.').next().unwrap_or(key);
537 if key == "_id" || writable.contains(root) {
538 result.insert(key.clone(), val.clone());
539 }
540 }
541 Value::Object(result)
542}
543
544pub fn is_field_readable_overlay(
547 registry: &Registry,
548 schema: &Schema,
549 ctx: Option<&Context>,
550 field: &str,
551) -> bool {
552 if !crate::permission::is_field_readable(registry.role_rules(), schema, ctx, field) {
553 return false;
554 }
555 let Some(p) = registry.rbac() else { return true };
556 let Some(c) = ctx else { return true };
557 if c.internal {
558 return true;
559 }
560 let Some(d) = decide(p, ®istry.role_rules().exempt_roles, Some(c), &schema.name) else { return true };
561 match &d.read_fields {
562 None => true,
563 Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
564 }
565}
566
567pub fn probe_condition_fields(
571 registry: &Registry,
572 schema: &Schema,
573 ctx: Option<&Context>,
574 action: &str,
575) -> Vec<String> {
576 let Some(p) = registry.rbac() else { return Vec::new() };
577 let Some(c) = ctx else { return Vec::new() };
578 if c.internal {
579 return Vec::new();
580 }
581 if decide(p, ®istry.role_rules().exempt_roles, Some(c), &schema.name).is_none() {
582 return Vec::new();
583 }
584 let roles = effective_roles(c);
585 let mut fields = Vec::new();
586 for g in p.grants.iter().filter(|g| {
587 g.actions.iter().any(|a| a == action)
588 && (g.model == schema.name || g.model == "*")
589 && roles.iter().any(|r| r == &g.role)
590 }) {
591 if let Some(cond) = &g.condition {
592 if let Some(obj) = cond.as_object() {
593 for k in obj.keys() {
594 if !fields.iter().any(|x: &String| x == k) {
595 fields.push(k.clone());
596 }
597 }
598 }
599 }
600 }
601 fields
602}
603
604fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
610 if !g.owner_only && g.condition.is_none() {
611 return None;
612 }
613 let owner = if g.owner_only {
614 let uid = c.user_id.clone().unwrap_or_default();
615 if uid.is_empty() {
616 Some(json!({ "__rbac_impossible__": true }))
618 } else {
619 Some(json!({ "createdBy": uid }))
620 }
621 } else {
622 None
623 };
624 match (owner, g.condition.clone()) {
625 (None, cond) => cond,
626 (owner, None) => owner,
627 (Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
628 }
629}
630
631fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
633 let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
634 return false;
635 };
636 cond_map.iter().all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
637}