1use openagent_aegis_core::{Delegation, DelegationError, DelegationScope};
9use std::collections::HashMap;
10
11use crate::scope::{intersect_scopes, is_scope_subset};
12
13pub struct DelegationTree {
18 delegations: HashMap<String, Vec<Delegation>>,
20 id_index: HashMap<String, (String, String)>,
22 max_depth: u32,
24}
25
26impl DelegationTree {
27 pub fn new(max_depth: u32) -> Self {
29 Self {
30 delegations: HashMap::new(),
31 id_index: HashMap::new(),
32 max_depth,
33 }
34 }
35
36 pub fn add_delegation(&mut self, delegation: Delegation) -> Result<(), DelegationError> {
43 let current_depth = self.depth(&delegation.delegator);
45 let new_depth = current_depth + 1;
46
47 if new_depth > self.max_depth {
48 return Err(DelegationError::MaxDepthExceeded {
49 depth: new_depth,
50 max_depth: self.max_depth,
51 });
52 }
53
54 if let Some(parent_scope) = self.effective_scope(&delegation.delegator) {
56 if !is_scope_subset(&delegation.scope, &parent_scope) {
57 return Err(DelegationError::ScopeAmplification {
58 reason: format!(
59 "delegate {} scope exceeds delegator {} effective scope",
60 delegation.delegate, delegation.delegator,
61 ),
62 });
63 }
64 }
65
66 self.id_index.insert(
68 delegation.id.clone(),
69 (delegation.delegator.clone(), delegation.delegate.clone()),
70 );
71
72 self.delegations
74 .entry(delegation.delegate.clone())
75 .or_default()
76 .push(delegation);
77
78 Ok(())
79 }
80
81 pub fn get_delegation_chain(&self, delegate_did: &str) -> Vec<&Delegation> {
86 let mut chain = Vec::new();
87 let mut current = delegate_did;
88
89 loop {
90 match self.delegations.get(current) {
91 Some(delegations) if !delegations.is_empty() => {
92 let delegation = &delegations[0];
94 chain.push(delegation);
95
96 let delegator = delegation.delegator.as_str();
98 if delegator == current {
99 break;
101 }
102 current = delegator;
103 }
104 _ => break,
105 }
106 }
107
108 chain
109 }
110
111 pub fn effective_scope(&self, delegate_did: &str) -> Option<DelegationScope> {
116 let chain = self.get_delegation_chain(delegate_did);
117 if chain.is_empty() {
118 return None;
119 }
120
121 let mut effective = chain[0].scope.clone();
122 for delegation in chain.iter().skip(1) {
123 effective = intersect_scopes(&effective, &delegation.scope);
124 }
125
126 Some(effective)
127 }
128
129 pub fn depth(&self, delegate_did: &str) -> u32 {
134 self.get_delegation_chain(delegate_did).len() as u32
135 }
136
137 pub fn revoke(&mut self, delegation_id: &str) -> Result<Vec<String>, DelegationError> {
142 let (_, delegate_did) =
144 self.id_index
145 .get(delegation_id)
146 .cloned()
147 .ok_or_else(|| DelegationError::Revoked {
148 delegation_id: delegation_id.to_string(),
149 })?;
150
151 let mut revoked_ids = Vec::new();
152
153 let mut dids_to_process = vec![delegate_did.clone()];
155 let mut processed_dids = std::collections::HashSet::new();
156
157 while let Some(did) = dids_to_process.pop() {
158 if !processed_dids.insert(did.clone()) {
159 continue;
160 }
161
162 for (sub_delegate_did, delegations) in &self.delegations {
165 for d in delegations {
166 if d.delegator == did && !processed_dids.contains(sub_delegate_did.as_str()) {
167 dids_to_process.push(sub_delegate_did.clone());
168 }
169 }
170 }
171 }
172
173 if let Some(delegations) = self.delegations.get_mut(&delegate_did) {
175 delegations.retain(|d| {
176 if d.id == delegation_id {
177 revoked_ids.push(d.id.clone());
178 false
179 } else {
180 true
181 }
182 });
183 if delegations.is_empty() {
184 self.delegations.remove(&delegate_did);
185 }
186 }
187 self.id_index.remove(delegation_id);
188
189 for did in &processed_dids {
192 if *did == delegate_did {
193 continue; }
195 if let Some(delegations) = self.delegations.get_mut(did) {
196 for d in delegations.iter() {
197 revoked_ids.push(d.id.clone());
198 self.id_index.remove(&d.id);
199 }
200 }
201 self.delegations.remove(did);
202 }
203
204 Ok(revoked_ids)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use openagent_aegis_core::DelegationProof;
212 use chrono::Utc;
213
214 fn make_delegation(
215 id: &str,
216 delegator: &str,
217 delegate: &str,
218 actions: Vec<&str>,
219 ) -> Delegation {
220 Delegation {
221 id: id.to_string(),
222 delegator: delegator.to_string(),
223 delegate: delegate.to_string(),
224 scope: DelegationScope {
225 actions: actions.into_iter().map(String::from).collect(),
226 resources: vec![],
227 chains: vec![],
228 limits: None,
229 temporal: None,
230 },
231 created: Utc::now(),
232 expires: None,
233 revocable: true,
234 proof: DelegationProof {
235 proof_type: "AegisDelegationProof2025".to_string(),
236 verification_method: format!("{delegator}#key"),
237 created: Utc::now(),
238 jws: "test-jws".to_string(),
239 },
240 }
241 }
242
243 #[test]
244 fn single_delegation_depth() {
245 let mut tree = DelegationTree::new(8);
246 let d = make_delegation("d1", "did:root", "did:agent1", vec!["transfer"]);
247 let result = tree.add_delegation(d);
248 assert!(result.is_ok());
249 assert_eq!(tree.depth("did:agent1"), 1);
250 assert_eq!(tree.depth("did:root"), 0);
251 }
252
253 #[test]
254 fn chain_depth_and_walk() {
255 let mut tree = DelegationTree::new(8);
256
257 let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer", "approve"]);
258 let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
259 let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
260
261 assert!(tree.add_delegation(d1).is_ok());
262 assert!(tree.add_delegation(d2).is_ok());
263 assert!(tree.add_delegation(d3).is_ok());
264
265 assert_eq!(tree.depth("did:c"), 3);
266
267 let chain = tree.get_delegation_chain("did:c");
268 assert_eq!(chain.len(), 3);
269 assert_eq!(chain[0].delegate, "did:c");
270 assert_eq!(chain[1].delegate, "did:b");
271 assert_eq!(chain[2].delegate, "did:a");
272 }
273
274 #[test]
275 fn max_depth_enforced() {
276 let mut tree = DelegationTree::new(2);
277
278 let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
279 let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
280 let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
281
282 assert!(tree.add_delegation(d1).is_ok());
283 assert!(tree.add_delegation(d2).is_ok());
284
285 let result = tree.add_delegation(d3);
287 assert!(result.is_err());
288 match result {
289 Err(DelegationError::MaxDepthExceeded { depth, max_depth }) => {
290 assert_eq!(depth, 3);
291 assert_eq!(max_depth, 2);
292 }
293 _ => panic!("expected MaxDepthExceeded"),
294 }
295 }
296
297 #[test]
298 fn scope_amplification_rejected() {
299 let mut tree = DelegationTree::new(8);
300
301 let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
302
303 let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
305
306 assert!(tree.add_delegation(d1).is_ok());
307
308 let result = tree.add_delegation(d2);
309 assert!(result.is_err());
310 match result {
311 Err(DelegationError::ScopeAmplification { .. }) => {}
312 other => panic!("expected ScopeAmplification, got {other:?}"),
313 }
314 }
315
316 #[test]
317 fn effective_scope_is_intersection() {
318 let mut tree = DelegationTree::new(8);
319
320 let d1 = make_delegation(
321 "d1",
322 "did:root",
323 "did:a",
324 vec!["transfer", "approve", "stake"],
325 );
326 let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
327
328 assert!(tree.add_delegation(d1).is_ok());
329 assert!(tree.add_delegation(d2).is_ok());
330
331 let scope = tree.effective_scope("did:b");
332 assert!(scope.is_some());
333 if let Some(s) = scope {
334 assert!(s.actions.contains(&"transfer".to_string()));
336 assert!(s.actions.contains(&"approve".to_string()));
337 assert!(!s.actions.contains(&"stake".to_string()));
338 }
339 }
340
341 #[test]
342 fn revocation_cascades() {
343 let mut tree = DelegationTree::new(8);
344
345 let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
346 let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
347 let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
348
349 assert!(tree.add_delegation(d1).is_ok());
350 assert!(tree.add_delegation(d2).is_ok());
351 assert!(tree.add_delegation(d3).is_ok());
352
353 let revoked = tree.revoke("d1");
355 assert!(revoked.is_ok());
356 if let Ok(ids) = revoked {
357 assert!(ids.contains(&"d1".to_string()));
358 assert!(ids.contains(&"d2".to_string()));
359 assert!(ids.contains(&"d3".to_string()));
360 }
361
362 assert_eq!(tree.depth("did:a"), 0);
364 assert_eq!(tree.depth("did:b"), 0);
365 assert_eq!(tree.depth("did:c"), 0);
366 }
367
368 #[test]
369 fn revoke_nonexistent_returns_error() {
370 let mut tree = DelegationTree::new(8);
371 let result = tree.revoke("nonexistent");
372 assert!(result.is_err());
373 }
374
375 #[test]
376 fn no_chain_for_unknown_did() {
377 let tree = DelegationTree::new(8);
378 let chain = tree.get_delegation_chain("did:unknown");
379 assert!(chain.is_empty());
380 assert!(tree.effective_scope("did:unknown").is_none());
381 }
382}