use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DelegationError {
#[error("delegator does not grant capability: {0}")]
CapabilityNotGranted(String),
#[error("requested TTL {requested}s exceeds delegator's remaining {available}s")]
TtlExceedsParent {
requested: u64,
available: u64,
},
#[error("delegation depth limit reached")]
DepthLimitExceeded,
}
pub struct DelegatorScope<'a> {
pub capabilities: &'a [String],
pub remaining_ttl_secs: u64,
pub depth: u32,
pub max_depth: u32,
}
pub struct RequestedScope<'a> {
pub capabilities: &'a [String],
pub ttl_secs: u64,
}
pub fn validate_delegation_constraints(
delegator: &DelegatorScope<'_>,
requested: &RequestedScope<'_>,
) -> Result<(), DelegationError> {
for cap in requested.capabilities {
if !delegator.capabilities.iter().any(|held| held == cap) {
return Err(DelegationError::CapabilityNotGranted(cap.clone()));
}
}
if requested.ttl_secs > delegator.remaining_ttl_secs {
return Err(DelegationError::TtlExceedsParent {
requested: requested.ttl_secs,
available: delegator.remaining_ttl_secs,
});
}
if delegator.depth >= delegator.max_depth {
return Err(DelegationError::DepthLimitExceeded);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capability_subset_valid() {
let parent = vec!["read".to_string(), "write".to_string()];
let child = vec!["read".to_string()];
let d = DelegatorScope {
capabilities: &parent,
remaining_ttl_secs: 3600,
depth: 0,
max_depth: 2,
};
let r = RequestedScope {
capabilities: &child,
ttl_secs: 3600,
};
assert!(validate_delegation_constraints(&d, &r).is_ok());
}
#[test]
fn capability_subset_invalid() {
let parent = vec!["read".to_string()];
let child = vec!["admin".to_string()];
let d = DelegatorScope {
capabilities: &parent,
remaining_ttl_secs: 3600,
depth: 0,
max_depth: 2,
};
let r = RequestedScope {
capabilities: &child,
ttl_secs: 3600,
};
assert_eq!(
validate_delegation_constraints(&d, &r),
Err(DelegationError::CapabilityNotGranted("admin".to_string()))
);
}
#[test]
fn ttl_exceeding_parent_is_rejected() {
let parent = vec!["read".to_string()];
let child = vec!["read".to_string()];
let d = DelegatorScope {
capabilities: &parent,
remaining_ttl_secs: 3600,
depth: 0,
max_depth: 2,
};
let r = RequestedScope {
capabilities: &child,
ttl_secs: 7200,
};
assert!(matches!(
validate_delegation_constraints(&d, &r),
Err(DelegationError::TtlExceedsParent { .. })
));
}
#[test]
fn depth_limit_is_rejected() {
let parent = vec!["read".to_string()];
let child = vec!["read".to_string()];
let d = DelegatorScope {
capabilities: &parent,
remaining_ttl_secs: 3600,
depth: 2,
max_depth: 2,
};
let r = RequestedScope {
capabilities: &child,
ttl_secs: 3600,
};
assert_eq!(
validate_delegation_constraints(&d, &r),
Err(DelegationError::DepthLimitExceeded)
);
}
}