1use std::fmt;
4
5use chrono::{DateTime, Utc};
6use wyrd_spec::auth::Attribution;
7use wyrd_spec::ids::IdempotencyKey;
8use wyrd_spec::request_id::RequestId;
9use wyrd_spec::vala::AuditDelegationStep;
10
11use crate::principal::{Principal, PrincipalRef};
12
13#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct DelegationStep {
17 pub principal: PrincipalRef,
19}
20
21impl DelegationStep {
22 #[must_use]
30 pub fn audit_projection(&self) -> AuditDelegationStep {
31 AuditDelegationStep {
32 principal_id: self.principal.id,
33 principal_kind: self.principal.kind.tag(),
34 card_ref: self.principal.card_ref().cloned(),
35 card_ref_scope: self
36 .principal
37 .card_ref_scope()
38 .map(|scope| scope.as_slice().to_vec())
39 .unwrap_or_default(),
40 }
41 }
42}
43
44#[must_use]
49pub fn audit_delegation_chain(chain: &[DelegationStep]) -> Vec<AuditDelegationStep> {
50 chain.iter().map(DelegationStep::audit_projection).collect()
51}
52
53#[must_use]
60pub fn delegated_attribution(principal: &Principal, chain: &[DelegationStep]) -> Attribution {
61 Attribution::new(
62 principal.id,
63 chain.last().map_or(principal.id, |step| step.principal.id),
64 )
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct AcceptedAuthority {
79 pub principal: Principal,
81 pub delegation_chain: Vec<DelegationStep>,
83}
84
85impl AcceptedAuthority {
86 #[must_use]
92 pub fn new(principal: &Principal, chain: &[DelegationStep]) -> Self {
93 Self {
94 principal: Principal {
95 credential_id: None,
96 ..principal.clone()
97 },
98 delegation_chain: chain.to_vec(),
99 }
100 }
101
102 #[must_use]
104 pub fn attribution(&self) -> Attribution {
105 delegated_attribution(&self.principal, &self.delegation_chain)
106 }
107}
108
109#[derive(Clone, serde::Serialize, serde::Deserialize)]
111pub struct RequestContext {
112 pub request_id: RequestId,
114 pub principal: Option<Principal>,
116 pub delegation_chain: Vec<DelegationStep>,
118 pub traceparent: Option<TraceParent>,
120 pub idempotency_key: Option<IdempotencyKey>,
122 pub started_at: DateTime<Utc>,
124}
125
126impl RequestContext {
127 #[must_use]
129 pub fn new(request_id: RequestId, started_at: DateTime<Utc>) -> Self {
130 Self {
131 request_id,
132 principal: None,
133 delegation_chain: Vec::new(),
134 traceparent: None,
135 idempotency_key: None,
136 started_at,
137 }
138 }
139
140 #[must_use]
142 pub fn principal(&self) -> Option<&Principal> {
143 self.principal.as_ref()
144 }
145
146 #[must_use]
148 pub fn is_delegated(&self) -> bool {
149 !self.delegation_chain.is_empty()
150 }
151}
152
153impl fmt::Debug for RequestContext {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.debug_struct("RequestContext")
156 .field("request_id", &self.request_id)
157 .field(
158 "principal",
159 &self.principal.as_ref().map(|principal| principal.id),
160 )
161 .field("delegation_chain_depth", &self.delegation_chain.len())
162 .field("traceparent", &self.traceparent)
163 .field(
164 "idempotency_key",
165 &self.idempotency_key.as_ref().map(|_| "<redacted>"),
166 )
167 .field("started_at", &self.started_at)
168 .finish()
169 }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
174#[serde(transparent)]
175pub struct TraceParent(String);
176
177impl TraceParent {
178 #[must_use]
180 pub fn new(value: impl Into<String>) -> Self {
181 Self(value.into())
182 }
183
184 #[must_use]
186 pub fn as_str(&self) -> &str {
187 &self.0
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::{AcceptedAuthority, DelegationStep, RequestContext, TraceParent};
194 use crate::permission::PermissionSet;
195 use crate::principal::{Principal, PrincipalId, PrincipalKind, PrincipalRef};
196 use chrono::Utc;
197 use wyrd_semver::VersionBlock;
198 use wyrd_spec::envelope::CardKind;
199 use wyrd_spec::ids::{CardName, IdempotencyKey, SpaceName};
200 use wyrd_spec::reference::{CardRef, CardRefScope};
201 use wyrd_spec::request_id::RequestId;
202
203 fn request_id() -> RequestId {
204 RequestId::parse(&uuid::Uuid::now_v7().to_string())
205 .expect("generated UUIDv7 is a valid request id")
206 }
207
208 fn card_ref(kind: CardKind, name: &str) -> CardRef {
209 CardRef {
210 kind,
211 name: CardName::new(name).expect("static card name is valid"),
212 version: VersionBlock::parse("1.0.0").expect("static version is valid"),
213 space: Some(SpaceName::new("prod").expect("static space is valid")),
214 uid: None,
215 }
216 }
217
218 fn service_principal(name: &str) -> Principal {
219 let card_ref = card_ref(CardKind::Service, name);
220 Principal {
221 id: PrincipalId::new(uuid::Uuid::now_v7()),
222 kind: PrincipalKind::Service {
223 card_ref: Some(card_ref.clone()),
224 card_ref_scope: CardRefScope::own(&card_ref),
225 },
226 tenant_id: wyrd_spec::DataTenantId::new_v7(),
227 roles: Vec::new(),
228 effective_permissions: PermissionSet::new(),
229 credential_id: None,
230 }
231 }
232
233 #[test]
234 fn delegation_chain_default_empty() {
235 let context = RequestContext::new(request_id(), Utc::now());
236
237 assert!(context.delegation_chain.is_empty());
238 assert!(!context.is_delegated());
239 }
240
241 #[test]
242 fn delegation_step_carries_self_describing_projection() {
243 let principal = service_principal("billing");
244 let step = DelegationStep {
245 principal: PrincipalRef::from_principal(&principal),
246 };
247
248 assert_eq!(step.principal.id, principal.id);
249 assert_eq!(step.principal.kind, principal.kind);
250 assert!(step.principal.card_ref().is_some());
251 }
252
253 #[test]
254 fn delegation_chain_round_trips() {
255 let mut context = RequestContext::new(request_id(), Utc::now());
256 context.principal = Some(service_principal("current"));
257 context.traceparent = Some(TraceParent::new(
258 "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01",
259 ));
260 context.delegation_chain = vec![
261 DelegationStep {
262 principal: PrincipalRef::from_principal(&service_principal("first")),
263 },
264 DelegationStep {
265 principal: PrincipalRef::from_principal(&service_principal("second")),
266 },
267 ];
268
269 let json = serde_json::to_string(&context).expect("serialize request context");
270 let parsed: RequestContext =
271 serde_json::from_str(&json).expect("deserialize request context");
272
273 assert_eq!(parsed.delegation_chain, context.delegation_chain);
274 assert_eq!(parsed.traceparent, context.traceparent);
275 assert!(parsed.is_delegated());
276 }
277
278 #[test]
279 fn debug_redacts_idempotency_key() {
280 let mut context = RequestContext::new(request_id(), Utc::now());
281 context.idempotency_key =
282 Some(IdempotencyKey::new("client-private-retry-key").expect("valid opaque key"));
283
284 let debug = format!("{context:?}");
285
286 assert!(debug.contains(r#"idempotency_key: Some("<redacted>")"#));
287 assert!(!debug.contains("client-private-retry-key"));
288 }
289
290 #[test]
291 fn debug_redacts_delegation_chain_to_depth() {
292 let mut context = RequestContext::new(request_id(), Utc::now());
293 context.delegation_chain = vec![
294 DelegationStep {
295 principal: PrincipalRef::from_principal(&service_principal("first")),
296 },
297 DelegationStep {
298 principal: PrincipalRef::from_principal(&service_principal("second")),
299 },
300 DelegationStep {
301 principal: PrincipalRef::from_principal(&service_principal("third")),
302 },
303 ];
304
305 let debug = format!("{context:?}");
306
307 assert!(debug.contains("delegation_chain_depth: 3"));
308 assert!(!debug.contains("first"));
309 assert!(!debug.contains("second"));
310 assert!(!debug.contains("third"));
311 }
312
313 #[test]
322 fn accepted_authority_round_trips_without_credential() {
323 let mut subject = service_principal("subject");
324 subject.credential_id = Some(uuid::Uuid::now_v7());
325 let actor = service_principal("actor");
326 let chain = vec![DelegationStep {
327 principal: PrincipalRef::from_principal(&actor),
328 }];
329
330 let accepted = AcceptedAuthority::new(&subject, &chain);
331 let json = serde_json::to_value(&accepted).expect("serialize accepted authority");
332 let parsed: AcceptedAuthority =
333 serde_json::from_value(json).expect("deserialize accepted authority");
334
335 assert_eq!(parsed, accepted);
336 assert_eq!(parsed.principal.credential_id, None);
337 assert_eq!(parsed.delegation_chain, chain);
338 assert_eq!(parsed.attribution().sub_principal_id, subject.id);
339 assert_eq!(parsed.attribution().act_principal_id, actor.id);
340 assert_eq!(
341 AcceptedAuthority::new(&subject, &[])
342 .attribution()
343 .act_principal_id,
344 subject.id
345 );
346 }
347
348 #[test]
356 fn accepted_authority_keeps_exact_tool_scope() {
357 use crate::permission::Permission;
358 use wyrd_spec::card::agent::BuiltinTool;
359 let query = Permission::tool_invoke(BuiltinTool::BifrostQuery);
360 let mut subject = service_principal("subject");
361 subject.effective_permissions = [Permission::tool_invoke_any()].into_iter().collect();
362 let actor_grants: PermissionSet = [query.clone()].into_iter().collect();
363 subject.effective_permissions = subject
364 .effective_permissions
365 .intersection(&actor_grants)
366 .expect("representable meet");
367 let chain = vec![DelegationStep {
368 principal: PrincipalRef::from_principal(&service_principal("actor")),
369 }];
370
371 let accepted = AcceptedAuthority::new(&subject, &chain);
372 let json = serde_json::to_value(&accepted).expect("serialize accepted authority");
373 let parsed: AcceptedAuthority =
374 serde_json::from_value(json).expect("deserialize accepted authority");
375
376 assert_eq!(parsed, accepted);
377 assert_eq!(parsed.principal.id, subject.id);
378 assert_eq!(parsed.delegation_chain, chain);
379 let grants = &parsed.principal.effective_permissions;
380 assert_eq!(grants.iter().collect::<Vec<_>>(), vec![&query]);
381 assert!(!grants.contains(&Permission::tool_invoke(BuiltinTool::FsRead)));
382 assert!(!grants.contains(&Permission::bifrost_query_read()));
383 }
384}