use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyDecision {
Permit,
Forbid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
pub id: String,
pub name: String,
pub action: String,
pub decision: PolicyDecision,
#[serde(default)]
pub priority: i32,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub scope: PolicyScope,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum PolicyScope {
#[default]
Global = 0,
Organization = 1,
Team = 2,
User = 3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Org {
pub id: String,
pub name: String,
#[serde(default)]
pub policies: Vec<Policy>,
#[serde(default)]
pub teams: Vec<Team>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
pub id: String,
pub name: String,
pub org_id: String,
#[serde(default)]
pub policies: Vec<Policy>,
#[serde(default)]
pub members: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantHierarchy {
#[serde(default)]
pub global_policies: Vec<Policy>,
#[serde(default)]
pub organizations: Vec<Org>,
}
impl TenantHierarchy {
pub fn new() -> Self {
Self {
global_policies: Vec::new(),
organizations: Vec::new(),
}
}
pub fn find_org(&self, org_id: &str) -> Option<&Org> {
self.organizations.iter().find(|o| o.id == org_id)
}
pub fn find_team(&self, org_id: &str, team_id: &str) -> Option<&Team> {
self.find_org(org_id)
.and_then(|org| org.teams.iter().find(|t| t.id == team_id))
}
pub fn find_user_team(&self, org_id: &str, user_id: &str) -> Option<&Team> {
self.find_org(org_id).and_then(|org| {
org.teams
.iter()
.find(|t| t.members.iter().any(|m| m == user_id))
})
}
}
impl Default for TenantHierarchy {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyResolutionOrder {
MostSpecificWins,
LeastSpecificWins,
}
pub fn resolve_effective_policies(
global_policies: &[Policy],
org_policies: &[Policy],
team_policies: &[Policy],
user_policies: &[Policy],
) -> Vec<Policy> {
resolve_with_order(
global_policies,
org_policies,
team_policies,
user_policies,
PolicyResolutionOrder::MostSpecificWins,
)
}
pub fn resolve_with_order(
global_policies: &[Policy],
org_policies: &[Policy],
team_policies: &[Policy],
user_policies: &[Policy],
_order: PolicyResolutionOrder,
) -> Vec<Policy> {
use std::collections::HashMap;
let mut by_action: HashMap<String, Vec<&Policy>> = HashMap::new();
for policy in global_policies
.iter()
.chain(org_policies.iter())
.chain(team_policies.iter())
.chain(user_policies.iter())
{
by_action
.entry(policy.action.clone())
.or_default()
.push(policy);
}
let mut effective = Vec::new();
for (action, policies) in &by_action {
let has_forbid = policies
.iter()
.any(|p| p.decision == PolicyDecision::Forbid);
if has_forbid {
let forbid_policy = policies
.iter()
.filter(|p| p.decision == PolicyDecision::Forbid)
.max_by(|a, b| {
a.scope
.cmp(&b.scope)
.then_with(|| a.priority.cmp(&b.priority))
})
.unwrap();
effective.push((*forbid_policy).clone());
} else {
let permit_policy = policies
.iter()
.filter(|p| p.decision == PolicyDecision::Permit)
.max_by(|a, b| {
a.scope
.cmp(&b.scope)
.then_with(|| a.priority.cmp(&b.priority))
});
if let Some(policy) = permit_policy {
effective.push((*policy).clone());
} else {
effective.push(Policy {
id: format!("default-deny-{}", action),
name: format!("Default deny for {}", action),
action: action.clone(),
decision: PolicyDecision::Forbid,
priority: 0,
description: Some("No explicit policy found; default deny".to_string()),
scope: PolicyScope::Global,
});
}
}
}
effective.sort_by(|a, b| a.action.cmp(&b.action));
effective
}
pub fn is_action_permitted(policies: &[Policy], action: &str) -> bool {
policies
.iter()
.find(|p| p.action == action)
.is_some_and(|p| p.decision == PolicyDecision::Permit)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_policy(
id: &str,
action: &str,
decision: PolicyDecision,
scope: PolicyScope,
priority: i32,
) -> Policy {
Policy {
id: id.to_string(),
name: format!("{} policy", id),
action: action.to_string(),
decision,
priority,
description: None,
scope,
}
}
#[test]
fn test_most_specific_wins_for_permits() {
let global = vec![make_policy(
"g1",
"Run",
PolicyDecision::Permit,
PolicyScope::Global,
0,
)];
let org = vec![make_policy(
"o1",
"Run",
PolicyDecision::Permit,
PolicyScope::Organization,
0,
)];
let team = vec![];
let user = vec![make_policy(
"u1",
"Run",
PolicyDecision::Permit,
PolicyScope::User,
0,
)];
let effective = resolve_effective_policies(&global, &org, &team, &user);
assert_eq!(effective.len(), 1);
assert_eq!(effective[0].id, "u1");
assert_eq!(effective[0].decision, PolicyDecision::Permit);
}
#[test]
fn test_forbid_always_overrides_permit() {
let global = vec![make_policy(
"g1",
"Network",
PolicyDecision::Forbid,
PolicyScope::Global,
0,
)];
let org = vec![];
let team = vec![];
let user = vec![make_policy(
"u1",
"Network",
PolicyDecision::Permit,
PolicyScope::User,
100,
)];
let effective = resolve_effective_policies(&global, &org, &team, &user);
assert_eq!(effective.len(), 1);
assert_eq!(effective[0].action, "Network");
assert_eq!(effective[0].decision, PolicyDecision::Forbid);
}
#[test]
fn test_forbid_at_any_level_wins() {
let global = vec![];
let org = vec![make_policy(
"o1",
"Mount",
PolicyDecision::Forbid,
PolicyScope::Organization,
0,
)];
let team = vec![make_policy(
"t1",
"Mount",
PolicyDecision::Permit,
PolicyScope::Team,
10,
)];
let user = vec![make_policy(
"u1",
"Mount",
PolicyDecision::Permit,
PolicyScope::User,
20,
)];
let effective = resolve_effective_policies(&global, &org, &team, &user);
assert_eq!(effective.len(), 1);
assert_eq!(effective[0].decision, PolicyDecision::Forbid);
}
#[test]
fn test_multiple_actions_resolved_independently() {
let global = vec![
make_policy("g1", "Run", PolicyDecision::Permit, PolicyScope::Global, 0),
make_policy(
"g2",
"Network",
PolicyDecision::Forbid,
PolicyScope::Global,
0,
),
];
let org = vec![];
let team = vec![];
let user = vec![make_policy(
"u1",
"Network",
PolicyDecision::Permit,
PolicyScope::User,
0,
)];
let effective = resolve_effective_policies(&global, &org, &team, &user);
assert_eq!(effective.len(), 2);
assert!(!is_action_permitted(&effective, "Network"));
assert!(is_action_permitted(&effective, "Run"));
}
#[test]
fn test_default_deny_when_no_policy() {
let effective = resolve_effective_policies(&[], &[], &[], &[]);
assert!(effective.is_empty());
assert!(!is_action_permitted(&effective, "Run"));
}
#[test]
fn test_priority_within_same_scope() {
let org = vec![
make_policy(
"o1",
"Run",
PolicyDecision::Permit,
PolicyScope::Organization,
10,
),
make_policy(
"o2",
"Run",
PolicyDecision::Permit,
PolicyScope::Organization,
20,
),
];
let effective = resolve_effective_policies(&[], &org, &[], &[]);
assert_eq!(effective.len(), 1);
assert_eq!(effective[0].id, "o2");
}
#[test]
fn test_tenant_hierarchy_find_org() {
let hierarchy = TenantHierarchy {
global_policies: vec![],
organizations: vec![
Org {
id: "acme".to_string(),
name: "Acme Corp".to_string(),
policies: vec![],
teams: vec![],
},
Org {
id: "globex".to_string(),
name: "Globex Inc".to_string(),
policies: vec![],
teams: vec![],
},
],
};
assert!(hierarchy.find_org("acme").is_some());
assert_eq!(hierarchy.find_org("acme").unwrap().name, "Acme Corp");
assert!(hierarchy.find_org("unknown").is_none());
}
#[test]
fn test_tenant_hierarchy_find_team() {
let hierarchy = TenantHierarchy {
global_policies: vec![],
organizations: vec![Org {
id: "acme".to_string(),
name: "Acme Corp".to_string(),
policies: vec![],
teams: vec![
Team {
id: "platform".to_string(),
name: "Platform Team".to_string(),
org_id: "acme".to_string(),
policies: vec![],
members: vec!["user-1".to_string(), "user-2".to_string()],
},
Team {
id: "ml".to_string(),
name: "ML Research".to_string(),
org_id: "acme".to_string(),
policies: vec![],
members: vec!["user-3".to_string()],
},
],
}],
};
assert!(hierarchy.find_team("acme", "platform").is_some());
assert_eq!(
hierarchy.find_team("acme", "platform").unwrap().name,
"Platform Team"
);
assert!(hierarchy.find_team("acme", "unknown").is_none());
assert!(hierarchy.find_team("unknown", "platform").is_none());
}
#[test]
fn test_tenant_hierarchy_find_user_team() {
let hierarchy = TenantHierarchy {
global_policies: vec![],
organizations: vec![Org {
id: "acme".to_string(),
name: "Acme Corp".to_string(),
policies: vec![],
teams: vec![
Team {
id: "platform".to_string(),
name: "Platform".to_string(),
org_id: "acme".to_string(),
policies: vec![],
members: vec!["alice".to_string(), "bob".to_string()],
},
Team {
id: "ml".to_string(),
name: "ML".to_string(),
org_id: "acme".to_string(),
policies: vec![],
members: vec!["carol".to_string()],
},
],
}],
};
let team = hierarchy.find_user_team("acme", "alice");
assert!(team.is_some());
assert_eq!(team.unwrap().id, "platform");
let team = hierarchy.find_user_team("acme", "carol");
assert!(team.is_some());
assert_eq!(team.unwrap().id, "ml");
assert!(hierarchy.find_user_team("acme", "unknown").is_none());
}
#[test]
fn test_policy_scope_ordering() {
assert!(PolicyScope::Global < PolicyScope::Organization);
assert!(PolicyScope::Organization < PolicyScope::Team);
assert!(PolicyScope::Team < PolicyScope::User);
}
#[test]
fn test_is_action_permitted() {
let policies = vec![
make_policy("p1", "Run", PolicyDecision::Permit, PolicyScope::User, 0),
make_policy(
"p2",
"Network",
PolicyDecision::Forbid,
PolicyScope::Global,
0,
),
];
assert!(is_action_permitted(&policies, "Run"));
assert!(!is_action_permitted(&policies, "Network"));
assert!(!is_action_permitted(&policies, "Unknown"));
}
#[test]
fn test_org_serialization() {
let org = Org {
id: "acme".to_string(),
name: "Acme Corp".to_string(),
policies: vec![make_policy(
"p1",
"Run",
PolicyDecision::Permit,
PolicyScope::Organization,
0,
)],
teams: vec![],
};
let json = serde_json::to_string(&org).unwrap();
let deserialized: Org = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, "acme");
assert_eq!(deserialized.policies.len(), 1);
}
}