cpex_core/extensions/
delegation.rs1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use super::authorization::AuthorizationDetail;
13use super::security::SubjectType;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[non_exhaustive]
25pub enum DelegationStrategy {
26 TokenExchange,
27 ClientCredentials,
28 SpiffeSvid,
29 Passthrough,
30 Ucan,
31 TransactionToken,
32 #[serde(untagged)]
33 Custom(String),
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct DelegationHop {
39 pub subject_id: String,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub subject_type: Option<SubjectType>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub audience: Option<String>,
50
51 #[serde(default)]
53 pub scopes_granted: Vec<String>,
54
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub authorization_details: Vec<AuthorizationDetail>,
59
60 #[serde(default)]
63 pub timestamp: DateTime<Utc>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub ttl_seconds: Option<u64>,
68
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub strategy: Option<DelegationStrategy>,
72
73 #[serde(default)]
75 pub from_cache: bool,
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
83pub struct DelegationExtension {
84 #[serde(default)]
86 pub chain: Vec<DelegationHop>,
87
88 #[serde(default)]
90 pub depth: u32,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub origin_subject_id: Option<String>,
95
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub actor_subject_id: Option<String>,
99
100 #[serde(default)]
102 pub delegated: bool,
103
104 #[serde(default)]
106 pub age_seconds: f64,
107}
108
109impl DelegationExtension {
110 pub fn append_hop(&mut self, hop: DelegationHop) {
112 self.chain.push(hop);
113 self.depth = self.chain.len() as u32;
116 self.delegated = true;
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_delegation_starts_empty() {
126 let del = DelegationExtension::default();
127 assert!(del.chain.is_empty());
128 assert_eq!(del.depth, 0);
129 assert!(!del.delegated);
130 }
131
132 #[test]
133 fn test_append_hop() {
134 let mut del = DelegationExtension::default();
135 del.append_hop(DelegationHop {
136 subject_id: "alice".into(),
137 scopes_granted: vec!["read_hr".into()],
138 ..Default::default()
139 });
140
141 assert_eq!(del.chain.len(), 1);
142 assert_eq!(del.depth, 1);
143 assert!(del.delegated);
144 assert_eq!(del.chain[0].subject_id, "alice");
145 assert_eq!(del.chain[0].scopes_granted, vec!["read_hr"]);
146 }
147
148 #[test]
149 fn test_append_multiple_hops() {
150 let mut del = DelegationExtension {
151 origin_subject_id: Some("alice".into()),
152 ..Default::default()
153 };
154
155 del.append_hop(DelegationHop {
156 subject_id: "alice".into(),
157 audience: Some("service-b".into()),
158 scopes_granted: vec!["read".into(), "write".into()],
159 strategy: Some(DelegationStrategy::TokenExchange),
160 ..Default::default()
161 });
162
163 del.append_hop(DelegationHop {
164 subject_id: "service-b".into(),
165 audience: Some("service-c".into()),
166 scopes_granted: vec!["read".into()], ..Default::default()
168 });
169
170 assert_eq!(del.chain.len(), 2);
171 assert_eq!(del.depth, 2);
172 assert_eq!(del.chain[1].scopes_granted, vec!["read"]);
174 }
175
176 #[test]
177 fn test_strategy_serde_known_and_custom() {
178 let known = DelegationStrategy::TokenExchange;
180 let json = serde_json::to_string(&known).unwrap();
181 assert_eq!(json, "\"token_exchange\"");
182 let back: DelegationStrategy = serde_json::from_str(&json).unwrap();
183 assert_eq!(back, DelegationStrategy::TokenExchange);
184
185 let custom = DelegationStrategy::Custom("in_house_mint".into());
187 let json = serde_json::to_string(&custom).unwrap();
188 assert_eq!(json, "\"in_house_mint\"");
189 let back: DelegationStrategy = serde_json::from_str("\"in_house_mint\"").unwrap();
192 assert_eq!(back, DelegationStrategy::Custom("in_house_mint".into()));
193 }
194
195 #[test]
196 fn test_delegation_serde_roundtrip() {
197 let mut del = DelegationExtension {
198 origin_subject_id: Some("alice".into()),
199 actor_subject_id: Some("service-b".into()),
200 ..Default::default()
201 };
202 del.append_hop(DelegationHop {
203 subject_id: "alice".into(),
204 subject_type: Some(SubjectType::User),
205 scopes_granted: vec!["admin".into()],
206 from_cache: true,
207 ..Default::default()
208 });
209
210 let json = serde_json::to_string(&del).unwrap();
211 let deserialized: DelegationExtension = serde_json::from_str(&json).unwrap();
212
213 assert_eq!(deserialized.depth, 1);
214 assert!(deserialized.delegated);
215 assert_eq!(deserialized.origin_subject_id.as_deref(), Some("alice"));
216 assert!(deserialized.chain[0].from_cache);
217 }
218}