use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::authorization::AuthorizationDetail;
use super::security::SubjectType;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DelegationStrategy {
TokenExchange,
ClientCredentials,
SpiffeSvid,
Passthrough,
Ucan,
TransactionToken,
#[serde(untagged)]
Custom(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DelegationHop {
pub subject_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject_type: Option<SubjectType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<String>,
#[serde(default)]
pub scopes_granted: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authorization_details: Vec<AuthorizationDetail>,
#[serde(default)]
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<DelegationStrategy>,
#[serde(default)]
pub from_cache: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DelegationExtension {
#[serde(default)]
pub chain: Vec<DelegationHop>,
#[serde(default)]
pub depth: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_subject_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_subject_id: Option<String>,
#[serde(default)]
pub delegated: bool,
#[serde(default)]
pub age_seconds: f64,
}
impl DelegationExtension {
pub fn append_hop(&mut self, hop: DelegationHop) {
self.chain.push(hop);
self.depth = self.chain.len() as u32;
self.delegated = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delegation_starts_empty() {
let del = DelegationExtension::default();
assert!(del.chain.is_empty());
assert_eq!(del.depth, 0);
assert!(!del.delegated);
}
#[test]
fn test_append_hop() {
let mut del = DelegationExtension::default();
del.append_hop(DelegationHop {
subject_id: "alice".into(),
scopes_granted: vec!["read_hr".into()],
..Default::default()
});
assert_eq!(del.chain.len(), 1);
assert_eq!(del.depth, 1);
assert!(del.delegated);
assert_eq!(del.chain[0].subject_id, "alice");
assert_eq!(del.chain[0].scopes_granted, vec!["read_hr"]);
}
#[test]
fn test_append_multiple_hops() {
let mut del = DelegationExtension {
origin_subject_id: Some("alice".into()),
..Default::default()
};
del.append_hop(DelegationHop {
subject_id: "alice".into(),
audience: Some("service-b".into()),
scopes_granted: vec!["read".into(), "write".into()],
strategy: Some(DelegationStrategy::TokenExchange),
..Default::default()
});
del.append_hop(DelegationHop {
subject_id: "service-b".into(),
audience: Some("service-c".into()),
scopes_granted: vec!["read".into()], ..Default::default()
});
assert_eq!(del.chain.len(), 2);
assert_eq!(del.depth, 2);
assert_eq!(del.chain[1].scopes_granted, vec!["read"]);
}
#[test]
fn test_strategy_serde_known_and_custom() {
let known = DelegationStrategy::TokenExchange;
let json = serde_json::to_string(&known).unwrap();
assert_eq!(json, "\"token_exchange\"");
let back: DelegationStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(back, DelegationStrategy::TokenExchange);
let custom = DelegationStrategy::Custom("in_house_mint".into());
let json = serde_json::to_string(&custom).unwrap();
assert_eq!(json, "\"in_house_mint\"");
let back: DelegationStrategy = serde_json::from_str("\"in_house_mint\"").unwrap();
assert_eq!(back, DelegationStrategy::Custom("in_house_mint".into()));
}
#[test]
fn test_delegation_serde_roundtrip() {
let mut del = DelegationExtension {
origin_subject_id: Some("alice".into()),
actor_subject_id: Some("service-b".into()),
..Default::default()
};
del.append_hop(DelegationHop {
subject_id: "alice".into(),
subject_type: Some(SubjectType::User),
scopes_granted: vec!["admin".into()],
from_cache: true,
..Default::default()
});
let json = serde_json::to_string(&del).unwrap();
let deserialized: DelegationExtension = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.depth, 1);
assert!(deserialized.delegated);
assert_eq!(deserialized.origin_subject_id.as_deref(), Some("alice"));
assert!(deserialized.chain[0].from_cache);
}
}