use serde::{Deserialize, Serialize};
use crate::Permission;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExponentialAttenuationPolicy {
pub decay_rate: f64,
pub admin_threshold: f64,
pub write_threshold: f64,
pub read_threshold: f64,
pub max_depth: usize,
}
impl Default for ExponentialAttenuationPolicy {
fn default() -> Self {
Self {
decay_rate: 0.5,
admin_threshold: 0.7,
write_threshold: 0.3,
read_threshold: 0.05,
max_depth: 20,
}
}
}
impl ExponentialAttenuationPolicy {
pub fn strength(&self, hops: usize) -> f64 {
#[allow(clippy::cast_precision_loss)] let h = hops as f64;
(-self.decay_rate * h).exp()
}
pub fn attenuate(&self, perm: Permission, hops: usize) -> Option<Permission> {
if hops > self.max_depth {
return None;
}
let s = self.strength(hops);
match perm {
Permission::Admin => {
if s >= self.admin_threshold {
Some(Permission::Admin)
} else if s >= self.write_threshold {
Some(Permission::Write)
} else if s >= self.read_threshold {
Some(Permission::Read)
} else {
None
}
},
Permission::Write => {
if s >= self.write_threshold {
Some(Permission::Write)
} else if s >= self.read_threshold {
Some(Permission::Read)
} else {
None
}
},
Permission::Read => {
if s >= self.read_threshold {
Some(Permission::Read)
} else {
None
}
},
}
}
}
#[derive(Debug, Clone)]
pub struct AttenuationPolicy {
pub admin_limit: usize,
pub write_limit: usize,
pub horizon: usize,
}
impl Default for AttenuationPolicy {
fn default() -> Self {
Self {
admin_limit: 1,
write_limit: 2,
horizon: 10,
}
}
}
impl AttenuationPolicy {
pub fn none() -> Self {
Self {
admin_limit: usize::MAX,
write_limit: usize::MAX,
horizon: usize::MAX,
}
}
pub fn attenuate(&self, perm: Permission, hops: usize) -> Option<Permission> {
if hops > self.horizon {
return None;
}
let attenuated = match perm {
Permission::Admin => {
if hops <= self.admin_limit {
Permission::Admin
} else if hops <= self.write_limit {
Permission::Write
} else {
Permission::Read
}
},
Permission::Write => {
if hops <= self.write_limit {
Permission::Write
} else {
Permission::Read
}
},
Permission::Read => Permission::Read,
};
Some(attenuated)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy_direct_admin_preserved() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 1),
Some(Permission::Admin)
);
}
#[test]
fn test_default_policy_2hop_admin_attenuated_to_write() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 2),
Some(Permission::Write)
);
}
#[test]
fn test_default_policy_3hop_admin_attenuated_to_read() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 3),
Some(Permission::Read)
);
}
#[test]
fn test_default_policy_beyond_max_hops_denied() {
let policy = AttenuationPolicy::default();
assert_eq!(policy.attenuate(Permission::Admin, 11), None);
assert_eq!(policy.attenuate(Permission::Write, 11), None);
assert_eq!(policy.attenuate(Permission::Read, 11), None);
}
#[test]
fn test_default_policy_write_preserved_at_2_hops() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Write, 1),
Some(Permission::Write)
);
assert_eq!(
policy.attenuate(Permission::Write, 2),
Some(Permission::Write)
);
}
#[test]
fn test_default_policy_write_attenuated_to_read_at_3_hops() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Write, 3),
Some(Permission::Read)
);
}
#[test]
fn test_default_policy_read_always_read() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Read, 1),
Some(Permission::Read)
);
assert_eq!(
policy.attenuate(Permission::Read, 5),
Some(Permission::Read)
);
assert_eq!(
policy.attenuate(Permission::Read, 10),
Some(Permission::Read)
);
}
#[test]
fn test_no_attenuation_policy() {
let policy = AttenuationPolicy::none();
assert_eq!(
policy.attenuate(Permission::Admin, 100),
Some(Permission::Admin)
);
assert_eq!(
policy.attenuate(Permission::Write, 100),
Some(Permission::Write)
);
assert_eq!(
policy.attenuate(Permission::Read, 100),
Some(Permission::Read)
);
}
#[test]
fn test_custom_policy() {
let policy = AttenuationPolicy {
admin_limit: 2,
write_limit: 4,
horizon: 6,
};
assert_eq!(
policy.attenuate(Permission::Admin, 2),
Some(Permission::Admin)
);
assert_eq!(
policy.attenuate(Permission::Admin, 3),
Some(Permission::Write)
);
assert_eq!(
policy.attenuate(Permission::Admin, 5),
Some(Permission::Read)
);
assert_eq!(policy.attenuate(Permission::Admin, 7), None);
}
#[test]
fn test_zero_hops() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 0),
Some(Permission::Admin)
);
}
#[test]
fn test_boundary_at_horizon() {
let policy = AttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Read, 10),
Some(Permission::Read)
);
assert_eq!(policy.attenuate(Permission::Read, 11), None);
}
#[test]
fn test_exp_strength_zero_hops() {
let policy = ExponentialAttenuationPolicy::default();
let s = policy.strength(0);
assert!(
(s - 1.0).abs() < f64::EPSILON,
"strength at 0 hops should be 1.0"
);
}
#[test]
fn test_exp_strength_decays() {
let policy = ExponentialAttenuationPolicy::default();
let s0 = policy.strength(0);
let s1 = policy.strength(1);
let s2 = policy.strength(2);
assert!(s0 > s1, "strength should decrease with hops");
assert!(s1 > s2, "strength should decrease with hops");
assert!(s2 > 0.0, "strength should remain positive");
}
#[test]
fn test_exp_admin_preserved() {
let policy = ExponentialAttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 0),
Some(Permission::Admin)
);
}
#[test]
fn test_exp_admin_to_write() {
let policy = ExponentialAttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 1),
Some(Permission::Write)
);
}
#[test]
fn test_exp_admin_to_read() {
let policy = ExponentialAttenuationPolicy::default();
assert_eq!(
policy.attenuate(Permission::Admin, 3),
Some(Permission::Read)
);
}
#[test]
fn test_exp_denied_below_threshold() {
let policy = ExponentialAttenuationPolicy::default();
assert_eq!(policy.attenuate(Permission::Admin, 6), None);
assert_eq!(policy.attenuate(Permission::Read, 6), None);
}
#[test]
fn test_exp_max_depth() {
let policy = ExponentialAttenuationPolicy::default();
assert_eq!(policy.attenuate(Permission::Admin, 21), None);
assert_eq!(policy.attenuate(Permission::Read, 21), None);
}
#[test]
fn test_exp_custom_decay_rate() {
let policy = ExponentialAttenuationPolicy {
decay_rate: 0.1,
admin_threshold: 0.7,
write_threshold: 0.3,
read_threshold: 0.05,
max_depth: 50,
};
assert_eq!(
policy.attenuate(Permission::Admin, 3),
Some(Permission::Admin)
);
assert_eq!(
policy.attenuate(Permission::Admin, 10),
Some(Permission::Write)
);
}
}