use crate::AbacError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Effect {
Allow,
Deny,
}
impl Default for Effect {
fn default() -> Self {
Self::Deny
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Condition {
RoleEquals(String),
ClearanceLevelAtLeast(u8),
DepartmentEquals(String),
TenantEquals(u64),
DataClassAtMost(String),
StreamNameMatches(String),
BusinessHoursOnly,
CountryIn(Vec<String>),
CountryNotIn(Vec<String>),
RetentionPeriodAtLeast(u32),
DataCorrectionAllowed,
IncidentReportingDeadline(u32),
FieldLevelRestriction(Vec<String>),
OperationalSequencing(Vec<String>),
LegalHoldActive,
And(Vec<Condition>),
Or(Vec<Condition>),
Not(Box<Condition>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
pub name: String,
pub effect: Effect,
pub conditions: Vec<Condition>,
pub priority: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AbacPolicy {
pub rules: Vec<Rule>,
pub default_effect: Effect,
}
impl Default for AbacPolicy {
fn default() -> Self {
Self {
rules: Vec::new(),
default_effect: Effect::Deny,
}
}
}
impl AbacPolicy {
pub fn new(default_effect: Effect) -> Self {
Self {
rules: Vec::new(),
default_effect,
}
}
pub fn with_rule(mut self, rule: Rule) -> Result<Self, AbacError> {
if self.rules.iter().any(|r| r.name == rule.name) {
return Err(AbacError::DuplicateRuleName(rule.name));
}
self.rules.push(rule);
Ok(self)
}
pub fn hipaa_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "hipaa-phi-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::BusinessHoursOnly,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "hipaa-non-phi-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn fedramp_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "fedramp-us-only".to_string(),
effect: Effect::Deny,
conditions: vec![Condition::CountryNotIn(vec!["US".to_string()])],
priority: 100,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "fedramp-allow-us".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(vec!["US".to_string()])],
priority: 50,
})
.expect("built-in policy has unique rule names")
}
pub fn pci_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "pci-server-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::DepartmentEquals("Server".to_string()),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "pci-non-pci-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn hitech_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "hitech-phi-minimum-necessary".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::BusinessHoursOnly,
Condition::FieldLevelRestriction(vec![
"patient_id".to_string(),
"patient_name".to_string(),
"patient_dob".to_string(),
]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "hitech-non-phi-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn cfr21_part11_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "cfr21-electronic-records".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(3),
Condition::OperationalSequencing(vec![
"Authorship".to_string(),
"Review".to_string(),
"Approval".to_string(),
]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "cfr21-read-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::ClearanceLevelAtLeast(2)],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn sox_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "sox-financial-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::RetentionPeriodAtLeast(2555),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "sox-non-financial-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn glba_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "glba-financial-us-only".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::CountryIn(vec!["US".to_string()]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "glba-non-financial-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn ccpa_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "ccpa-pii-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(1),
Condition::DataCorrectionAllowed,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "ccpa-non-pii-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn ferpa_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "ferpa-student-data".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::BusinessHoursOnly,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "ferpa-non-pii-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Confidential".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn nist_800_53_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "nist-sensitive-us-only".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::CountryIn(vec!["US".to_string()]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "nist-public-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Public".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn cmmc_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "cmmc-controlled-us-only".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::CountryIn(vec!["US".to_string()]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "cmmc-public-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Public".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn legal_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "legal-hold-block-deletion".to_string(),
effect: Effect::Deny,
conditions: vec![Condition::LegalHoldActive],
priority: 100,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "legal-confidential-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::ClearanceLevelAtLeast(2)],
priority: 10,
})
.expect("built-in policy has unique rule names")
}
pub fn nis2_policy() -> Self {
let eu_countries = vec![
"DE".to_string(),
"FR".to_string(),
"NL".to_string(),
"IT".to_string(),
"ES".to_string(),
"BE".to_string(),
"AT".to_string(),
"PT".to_string(),
"IE".to_string(),
"FI".to_string(),
"SE".to_string(),
"DK".to_string(),
"PL".to_string(),
"CZ".to_string(),
"RO".to_string(),
"BG".to_string(),
"HR".to_string(),
"SK".to_string(),
"SI".to_string(),
"LT".to_string(),
"LV".to_string(),
"EE".to_string(),
"CY".to_string(),
"MT".to_string(),
"LU".to_string(),
"HU".to_string(),
"GR".to_string(),
];
Self::new(Effect::Deny)
.with_rule(Rule {
name: "nis2-eu-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::CountryIn(eu_countries),
Condition::IncidentReportingDeadline(24),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
}
pub fn dora_policy() -> Self {
let eu_countries = vec![
"DE".to_string(),
"FR".to_string(),
"NL".to_string(),
"IT".to_string(),
"ES".to_string(),
"BE".to_string(),
"AT".to_string(),
"PT".to_string(),
"IE".to_string(),
"FI".to_string(),
"SE".to_string(),
"DK".to_string(),
"PL".to_string(),
"CZ".to_string(),
"RO".to_string(),
"BG".to_string(),
"HR".to_string(),
"SK".to_string(),
"SI".to_string(),
"LT".to_string(),
"LV".to_string(),
"EE".to_string(),
"CY".to_string(),
"MT".to_string(),
"LU".to_string(),
"HU".to_string(),
"GR".to_string(),
];
Self::new(Effect::Deny)
.with_rule(Rule {
name: "dora-financial-eu-only".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::CountryIn(eu_countries.clone()),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "dora-non-financial-eu".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(eu_countries)],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn eidas_policy() -> Self {
let eu_countries = vec![
"DE".to_string(),
"FR".to_string(),
"NL".to_string(),
"IT".to_string(),
"ES".to_string(),
"BE".to_string(),
"AT".to_string(),
"PT".to_string(),
"IE".to_string(),
"FI".to_string(),
"SE".to_string(),
"DK".to_string(),
"PL".to_string(),
"CZ".to_string(),
"RO".to_string(),
"BG".to_string(),
"HR".to_string(),
"SK".to_string(),
"SI".to_string(),
"LT".to_string(),
"LV".to_string(),
"EE".to_string(),
"CY".to_string(),
"MT".to_string(),
"LU".to_string(),
"HU".to_string(),
"GR".to_string(),
];
Self::new(Effect::Deny)
.with_rule(Rule {
name: "eidas-qualified-signatures".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::CountryIn(eu_countries.clone()),
Condition::OperationalSequencing(vec![
"Identification".to_string(),
"Authentication".to_string(),
"Signing".to_string(),
]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "eidas-basic-eu-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(eu_countries)],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn gdpr_policy() -> Self {
let eu_countries = vec![
"DE".to_string(),
"FR".to_string(),
"NL".to_string(),
"IT".to_string(),
"ES".to_string(),
"BE".to_string(),
"AT".to_string(),
"PT".to_string(),
"IE".to_string(),
"FI".to_string(),
"SE".to_string(),
"DK".to_string(),
"PL".to_string(),
"CZ".to_string(),
"RO".to_string(),
"BG".to_string(),
"HR".to_string(),
"SK".to_string(),
"SI".to_string(),
"LT".to_string(),
"LV".to_string(),
"EE".to_string(),
"CY".to_string(),
"MT".to_string(),
"LU".to_string(),
"HU".to_string(),
"GR".to_string(),
];
Self::new(Effect::Deny)
.with_rule(Rule {
name: "gdpr-pii-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(1),
Condition::CountryIn(eu_countries.clone()),
Condition::DataCorrectionAllowed,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "gdpr-non-pii-eu".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(eu_countries)],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn iso27001_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "iso27001-confidential-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::BusinessHoursOnly,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "iso27001-public-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::DataClassAtMost("Public".to_string())],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn aus_privacy_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "aus-privacy-pii-access".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(1),
Condition::CountryIn(vec!["AU".to_string()]),
Condition::DataCorrectionAllowed,
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "aus-privacy-non-pii".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(vec!["AU".to_string()])],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
pub fn apra_cps234_policy() -> Self {
Self::new(Effect::Deny)
.with_rule(Rule {
name: "apra-financial-au-only".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::And(vec![
Condition::ClearanceLevelAtLeast(2),
Condition::CountryIn(vec!["AU".to_string()]),
])],
priority: 10,
})
.expect("built-in policy has unique rule names")
.with_rule(Rule {
name: "apra-non-financial-au".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::CountryIn(vec!["AU".to_string()])],
priority: 5,
})
.expect("built-in policy has unique rule names")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_effect_is_deny() {
let policy = AbacPolicy::default();
assert_eq!(policy.default_effect, Effect::Deny);
}
#[test]
fn test_hipaa_policy_structure() {
let policy = AbacPolicy::hipaa_policy();
assert_eq!(policy.default_effect, Effect::Deny);
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.rules[0].name, "hipaa-phi-access");
assert_eq!(policy.rules[0].priority, 10);
assert_eq!(policy.rules[0].effect, Effect::Allow);
assert_eq!(policy.rules[1].name, "hipaa-non-phi-access");
assert_eq!(policy.rules[1].priority, 5);
}
#[test]
fn test_fedramp_policy_structure() {
let policy = AbacPolicy::fedramp_policy();
assert_eq!(policy.default_effect, Effect::Deny);
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.rules[0].name, "fedramp-us-only");
assert_eq!(policy.rules[0].effect, Effect::Deny);
assert!(policy.rules[0].priority > policy.rules[1].priority);
}
#[test]
fn test_pci_policy_structure() {
let policy = AbacPolicy::pci_policy();
assert_eq!(policy.default_effect, Effect::Deny);
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.rules[0].name, "pci-server-access");
assert_eq!(policy.rules[0].effect, Effect::Allow);
}
#[test]
fn test_with_rule_builder() {
let policy = AbacPolicy::new(Effect::Allow)
.with_rule(Rule {
name: "rule-a".to_string(),
effect: Effect::Deny,
conditions: vec![Condition::BusinessHoursOnly],
priority: 1,
})
.unwrap()
.with_rule(Rule {
name: "rule-b".to_string(),
effect: Effect::Allow,
conditions: vec![Condition::ClearanceLevelAtLeast(1)],
priority: 2,
})
.unwrap();
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.default_effect, Effect::Allow);
}
#[test]
fn with_rule_rejects_duplicate_names() {
let rule_a = Rule {
name: "x".into(),
effect: Effect::Allow,
conditions: vec![],
priority: 10,
};
let rule_b = Rule {
name: "x".into(),
effect: Effect::Deny,
conditions: vec![],
priority: 5,
};
let result = AbacPolicy::new(Effect::Deny)
.with_rule(rule_a)
.unwrap()
.with_rule(rule_b);
assert!(matches!(result, Err(AbacError::DuplicateRuleName(name)) if name == "x"));
}
#[test]
fn test_condition_serialization_roundtrip() {
let condition = Condition::And(vec![
Condition::RoleEquals("admin".to_string()),
Condition::Not(Box::new(Condition::BusinessHoursOnly)),
]);
let json = serde_json::to_string(&condition).expect("serialize condition");
let deserialized: Condition = serde_json::from_str(&json).expect("deserialize condition");
assert_eq!(condition, deserialized);
}
#[test]
fn test_policy_serialization_roundtrip() {
let policy = AbacPolicy::hipaa_policy();
let json = serde_json::to_string(&policy).expect("serialize policy");
let deserialized: AbacPolicy = serde_json::from_str(&json).expect("deserialize policy");
assert_eq!(deserialized.default_effect, policy.default_effect);
assert_eq!(deserialized.rules.len(), policy.rules.len());
}
}