use std::collections::HashSet;
use serde_json::{json, Map, Value};
use crate::permission::{get_writable_fields, Context};
use crate::schema::{Registry, Schema};
use crate::types::is_truthy;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RbacMode {
#[default]
Overlay,
Enforce,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteAction {
Insert,
Update,
Remove,
}
impl WriteAction {
pub fn as_str(&self) -> &'static str {
match self {
WriteAction::Insert => "insert",
WriteAction::Update => "update",
WriteAction::Remove => "remove",
}
}
}
pub fn write_action_from_str(s: &str) -> Result<WriteAction, String> {
match s {
"insert" => Ok(WriteAction::Insert),
"update" => Ok(WriteAction::Update),
"remove" => Ok(WriteAction::Remove),
other => Err(format!(
"RBAC action \"{other}\" 非法(仅支持 read / insert / update / remove)"
)),
}
}
#[derive(Debug, Clone)]
pub struct Grant {
role: String,
model: String, actions: Vec<String>, read_fields: Option<HashSet<String>>,
write_fields: Option<HashSet<String>>,
owner_only: bool,
condition: Option<Value>, }
#[derive(Debug, Clone, Default)]
pub struct RbacPolicy {
mode: RbacMode,
managed_roles: HashSet<String>,
grants: Vec<Grant>,
}
#[derive(Debug, Clone)]
pub struct ModelDecision {
pub allowed_actions: HashSet<String>, pub read_fields: Option<HashSet<String>>,
pub write_fields: Option<HashSet<String>>,
}
pub fn deny_msg(action: &str, model: &str) -> String {
format!("ERR_PERMISSION:RBAC:角色对 [{model}] 无 [{action}] 权限")
}
impl RbacPolicy {
pub fn from_json(v: &Value) -> Result<Self, String> {
let obj = v
.as_object()
.ok_or_else(|| "RBAC 策略必须是对象".to_string())?;
let mode = match obj.get("mode") {
None | Some(Value::Null) => RbacMode::Overlay,
Some(Value::String(s)) if s == "overlay" => RbacMode::Overlay,
Some(Value::String(s)) if s == "enforce" => RbacMode::Enforce,
Some(other) => {
return Err(format!(
"RBAC mode {other} 非法(仅支持 overlay / enforce)"
))
}
};
let mut managed_roles = HashSet::new();
match obj.get("roles") {
None | Some(Value::Null) => {}
Some(Value::Object(rm)) => {
for (name, _) in rm {
if name.is_empty() {
return Err("RBAC roles 含空角色名".to_string());
}
managed_roles.insert(name.clone());
}
}
Some(other) => return Err(format!("RBAC roles 必须是对象,实际 {other}")),
}
if mode == RbacMode::Enforce && managed_roles.is_empty() {
return Err(
"RBAC mode=enforce 时 roles 不能为空(无受管角色 = 策略空转,属配置错误)"
.to_string(),
);
}
let mut grants = Vec::new();
match obj.get("grants") {
None | Some(Value::Null) => {}
Some(Value::Array(gs)) => {
for (i, g) in gs.iter().enumerate() {
grants.push(parse_grant(i, g)?);
}
}
Some(other) => return Err(format!("RBAC grants 必须是数组,实际 {other}")),
}
Ok(Self {
mode,
managed_roles,
grants,
})
}
}
fn parse_grant(idx: usize, v: &Value) -> Result<Grant, String> {
let o = v
.as_object()
.ok_or_else(|| format!("RBAC grants[{idx}] 必须是对象"))?;
let get_str = |k: &str| -> Result<String, String> {
o.get(k)
.and_then(|x| x.as_str())
.filter(|s| !s.is_empty())
.map(String::from)
.ok_or_else(|| format!("RBAC grants[{idx}].{k} 必须是非空字符串"))
};
let role = get_str("role")?;
let model = get_str("model")?;
let actions_raw = o
.get("actions")
.and_then(|x| x.as_array())
.ok_or_else(|| format!("RBAC grants[{idx}].actions 必须是非空数组"))?;
if actions_raw.is_empty() {
return Err(format!(
"RBAC grants[{idx}].actions 不能为空(不授权请移除该 grant)"
));
}
let mut actions: Vec<String> = Vec::new();
for a in actions_raw {
let s = a
.as_str()
.ok_or_else(|| format!("RBAC grants[{idx}].actions 含非字符串项 {a}"))?;
let expanded: &[&str] = match s {
"read" => &["read"],
"insert" => &["insert"],
"update" => &["update"],
"remove" => &["remove"],
"write" => &["insert", "update", "remove"],
other => {
return Err(format!(
"RBAC grants[{idx}].actions 含非法值 \"{other}\"(仅支持 read/insert/update/remove/write)"
))
}
};
for e in expanded {
if !actions.iter().any(|x| x == e) {
actions.push((*e).to_string());
}
}
}
let parse_fields = |k: &str| -> Result<Option<HashSet<String>>, String> {
match o.get(k) {
None | Some(Value::Null) => Ok(None),
Some(Value::Array(fs)) => {
if fs.is_empty() {
return Err(format!(
"RBAC grants[{idx}].{k} 不能为空数组(全禁请移除该 grant)"
));
}
let mut set = HashSet::new();
for f in fs {
let s = f.as_str().filter(|s| !s.is_empty()).ok_or_else(|| {
format!("RBAC grants[{idx}].{k} 含非法项 {f}(须非空字符串)")
})?;
set.insert(s.to_string());
}
Ok(Some(set))
}
Some(other) => Err(format!(
"RBAC grants[{idx}].{k} 必须是字符串数组,实际 {other}"
)),
}
};
let read_fields = parse_fields("readFields")?;
let write_fields = parse_fields("writeFields")?;
let owner_only = o.get("ownerOnly").map(is_truthy).unwrap_or(false);
let condition = match o.get("condition") {
None | Some(Value::Null) => None,
Some(cm @ Value::Object(_)) => {
let entries = cm.as_object().expect("已按 Object 匹配,此处必为对象");
for (k, val) in entries {
if k.is_empty() || k.starts_with('$') {
return Err(format!(
"RBAC grants[{idx}].condition 键 \"{k}\" 非法(第一批仅支持等值字段条件,键不得以 $ 开头)"
));
}
if !val.is_boolean() && !val.is_number() && !val.is_string() && !val.is_null() {
return Err(format!(
"RBAC grants[{idx}].condition[\"{k}\"] 必须是标量(string/number/bool/null),实际 {val}"
));
}
}
Some(cm.clone())
}
Some(other) => {
return Err(format!(
"RBAC grants[{idx}].condition 必须是对象(等值条件),实际 {other}"
))
}
};
Ok(Grant {
role,
model,
actions,
read_fields,
write_fields,
owner_only,
condition,
})
}
fn effective_roles(c: &Context) -> Vec<String> {
match &c.roles {
Some(r) if !r.is_empty() => r.clone(),
_ => vec![c.role.clone().unwrap_or_default()],
}
}
pub fn decide(
policy: &RbacPolicy,
exempt_roles: &[String],
ctx: Option<&Context>,
model: &str,
) -> Option<ModelDecision> {
let c = ctx?;
if c.internal {
return None;
}
let roles = effective_roles(c);
if roles.iter().any(|r| exempt_roles.iter().any(|x| x == r)) {
return None;
}
let matched: Vec<&Grant> = policy
.grants
.iter()
.filter(|g| roles.iter().any(|r| r == &g.role) && (g.model == model || g.model == "*"))
.collect();
match policy.mode {
RbacMode::Overlay if matched.is_empty() => return None,
RbacMode::Enforce => {
let managed = roles.iter().any(|r| policy.managed_roles.contains(r));
if !managed {
return None;
}
}
_ => {}
}
let mut allowed_actions = HashSet::new();
let mut read_all = false;
let mut read_union: HashSet<String> = HashSet::new();
let mut write_all = false;
let mut write_union: HashSet<String> = HashSet::new();
for g in &matched {
for a in &g.actions {
allowed_actions.insert(a.clone());
}
if g.actions.iter().any(|a| a == "read") {
match &g.read_fields {
None => read_all = true,
Some(s) => read_union.extend(s.iter().cloned()),
}
}
if g.actions
.iter()
.any(|a| a == "insert" || a == "update" || a == "remove")
{
match &g.write_fields {
None => write_all = true,
Some(s) => write_union.extend(s.iter().cloned()),
}
}
}
let read_fields = if read_all || read_union.is_empty() {
None
} else {
Some(read_union)
};
let write_fields = if write_all || write_union.is_empty() {
None
} else {
Some(write_union)
};
Some(ModelDecision {
allowed_actions,
read_fields,
write_fields,
})
}
pub fn ensure_read(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
) -> Result<(), String> {
let Some(p) = registry.rbac() else {
return Ok(());
};
match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
None => Ok(()),
Some(d) if d.allowed_actions.contains("read") => Ok(()),
Some(_) => Err(deny_msg("read", &schema.name)),
}
}
pub fn ensure_write(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
action: WriteAction,
) -> Result<(), String> {
let Some(p) = registry.rbac() else {
return Ok(());
};
match decide(p, ®istry.role_rules().exempt_roles, ctx, &schema.name) {
None => Ok(()),
Some(d) if d.allowed_actions.contains(action.as_str()) => Ok(()),
Some(_) => Err(deny_msg(action.as_str(), &schema.name)),
}
}
pub fn ensure_write_on_doc(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
action: WriteAction,
doc: &Value,
) -> Result<(), String> {
let Some(p) = registry.rbac() else {
return Ok(());
};
let Some(c) = ctx else { return Ok(()) };
if c.internal {
return Ok(());
}
let Some(d) = decide(
p,
®istry.role_rules().exempt_roles,
Some(c),
&schema.name,
) else {
return Ok(());
};
if !d.allowed_actions.contains(action.as_str()) {
return Err(deny_msg(action.as_str(), &schema.name));
}
let roles = effective_roles(c);
let hit = p.grants.iter().any(|g| {
g.actions.iter().any(|a| a == action.as_str())
&& (g.model == schema.name || g.model == "*")
&& roles.iter().any(|r| r == &g.role)
&& match grant_row_restriction(g, c) {
None => true,
Some(cond) => doc_matches_eq(doc, &cond),
}
});
if hit {
Ok(())
} else {
Err(format!(
"ERR_PERMISSION:RBAC:角色对 [{}] 的 [{}] 限于本人/条件内文档",
schema.name,
action.as_str()
))
}
}
pub fn row_condition(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
action: &str,
) -> Option<Value> {
let p = registry.rbac()?;
let c = ctx?;
if c.internal {
return None;
}
decide(
p,
®istry.role_rules().exempt_roles,
Some(c),
&schema.name,
)?;
let roles = effective_roles(c);
let mut conds: Vec<Value> = Vec::new();
for g in p.grants.iter().filter(|g| {
g.actions.iter().any(|a| a == action)
&& (g.model == schema.name || g.model == "*")
&& roles.iter().any(|r| r == &g.role)
}) {
match grant_row_restriction(g, c) {
None => return None, Some(v) => conds.push(v),
}
}
match conds.len() {
0 => None,
1 => Some(conds.pop().unwrap_or(Value::Null)),
_ => Some(json!({ "$or": conds })),
}
}
pub fn merge_row_condition(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
action: &str,
base: Option<Value>,
) -> Option<Value> {
let meaningful = |v: Option<Value>| -> Option<Value> {
v.filter(|x| x.as_object().map(|o| !o.is_empty()).unwrap_or(false))
};
let rbac_cond = row_condition(registry, schema, ctx, action);
match (meaningful(base), rbac_cond) {
(None, r) => r,
(b, None) => b,
(Some(b), Some(r)) => Some(json!({ "$and": [b, r] })),
}
}
pub fn overlay_readable_fields(
registry: &Registry,
model: &str,
ctx: Option<&Context>,
base: Option<HashSet<String>>,
) -> Option<HashSet<String>> {
let Some(p) = registry.rbac() else {
return base;
};
let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
return base;
};
let Some(rf) = &d.read_fields else {
return base;
};
match base {
None => {
let mut s: HashSet<String> = rf.clone();
s.insert("_id".to_string());
Some(s)
}
Some(b) => {
let keep_id = b.contains("_id");
let mut s: HashSet<String> = b.intersection(rf).cloned().collect();
if keep_id {
s.insert("_id".to_string());
}
Some(s)
}
}
}
pub fn overlay_writable_fields(
registry: &Registry,
model: &str,
ctx: Option<&Context>,
base: Option<HashSet<String>>,
) -> Option<HashSet<String>> {
let Some(p) = registry.rbac() else {
return base;
};
let Some(d) = decide(p, ®istry.role_rules().exempt_roles, ctx, model) else {
return base;
};
let Some(wf) = &d.write_fields else {
return base;
};
match base {
None => {
let mut s: HashSet<String> = wf.clone();
s.insert("_id".to_string());
Some(s)
}
Some(b) => {
let keep_id = b.contains("_id");
let mut s: HashSet<String> = b.intersection(wf).cloned().collect();
if keep_id {
s.insert("_id".to_string());
}
Some(s)
}
}
}
pub fn filter_writable_data_overlay(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
data: &Value,
) -> Value {
if registry.rbac().is_none() {
return crate::permission::filter_writable_data(registry.role_rules(), schema, ctx, data);
}
let base = get_writable_fields(registry.role_rules(), schema, ctx);
let writable = overlay_writable_fields(registry, &schema.name, ctx, base);
let Some(writable) = writable else {
return data.clone();
};
let Some(obj) = data.as_object() else {
return data.clone();
};
let mut result = Map::new();
for (key, val) in obj {
let root = key.split('.').next().unwrap_or(key);
if key == "_id" || writable.contains(root) {
result.insert(key.clone(), val.clone());
}
}
Value::Object(result)
}
pub fn is_field_readable_overlay(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
field: &str,
) -> bool {
if !crate::permission::is_field_readable(registry.role_rules(), schema, ctx, field) {
return false;
}
let Some(p) = registry.rbac() else {
return true;
};
let Some(c) = ctx else { return true };
if c.internal {
return true;
}
let Some(d) = decide(
p,
®istry.role_rules().exempt_roles,
Some(c),
&schema.name,
) else {
return true;
};
match &d.read_fields {
None => true,
Some(rf) => rf.contains(field.split('.').next().unwrap_or(field)),
}
}
pub fn probe_condition_fields(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
action: &str,
) -> Vec<String> {
let Some(p) = registry.rbac() else {
return Vec::new();
};
let Some(c) = ctx else { return Vec::new() };
if c.internal {
return Vec::new();
}
if decide(
p,
®istry.role_rules().exempt_roles,
Some(c),
&schema.name,
)
.is_none()
{
return Vec::new();
}
let roles = effective_roles(c);
let mut fields = Vec::new();
for g in p.grants.iter().filter(|g| {
g.actions.iter().any(|a| a == action)
&& (g.model == schema.name || g.model == "*")
&& roles.iter().any(|r| r == &g.role)
}) {
if let Some(cond) = &g.condition {
if let Some(obj) = cond.as_object() {
for k in obj.keys() {
if !fields.iter().any(|x: &String| x == k) {
fields.push(k.clone());
}
}
}
}
}
fields
}
fn grant_row_restriction(g: &Grant, c: &Context) -> Option<Value> {
if !g.owner_only && g.condition.is_none() {
return None;
}
let owner = if g.owner_only {
let uid = c.user_id.clone().unwrap_or_default();
if uid.is_empty() {
Some(json!({ "__rbac_impossible__": true }))
} else {
Some(json!({ "createdBy": uid }))
}
} else {
None
};
match (owner, g.condition.clone()) {
(None, cond) => cond,
(owner, None) => owner,
(Some(o), Some(cond)) => Some(json!({ "$and": [o, cond] })),
}
}
fn doc_matches_eq(doc: &Value, cond: &Value) -> bool {
let (Some(obj), Some(cond_map)) = (doc.as_object(), cond.as_object()) else {
return false;
};
cond_map
.iter()
.all(|(k, v)| obj.get(k).map(|dv| dv == v).unwrap_or(false))
}