use std::fmt;
use chrono::{DateTime, Utc};
use wyrd_spec::auth::Attribution;
use wyrd_spec::ids::IdempotencyKey;
use wyrd_spec::request_id::RequestId;
use wyrd_spec::vala::AuditDelegationStep;
use crate::principal::{Principal, PrincipalRef};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DelegationStep {
pub principal: PrincipalRef,
}
impl DelegationStep {
#[must_use]
pub fn audit_projection(&self) -> AuditDelegationStep {
AuditDelegationStep {
principal_id: self.principal.id,
principal_kind: self.principal.kind.tag(),
card_ref: self.principal.card_ref().cloned(),
card_ref_scope: self
.principal
.card_ref_scope()
.map(|scope| scope.as_slice().to_vec())
.unwrap_or_default(),
}
}
}
#[must_use]
pub fn audit_delegation_chain(chain: &[DelegationStep]) -> Vec<AuditDelegationStep> {
chain.iter().map(DelegationStep::audit_projection).collect()
}
#[must_use]
pub fn delegated_attribution(principal: &Principal, chain: &[DelegationStep]) -> Attribution {
Attribution::new(
principal.id,
chain.last().map_or(principal.id, |step| step.principal.id),
)
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AcceptedAuthority {
pub principal: Principal,
pub delegation_chain: Vec<DelegationStep>,
}
impl AcceptedAuthority {
#[must_use]
pub fn new(principal: &Principal, chain: &[DelegationStep]) -> Self {
Self {
principal: Principal {
credential_id: None,
..principal.clone()
},
delegation_chain: chain.to_vec(),
}
}
#[must_use]
pub fn attribution(&self) -> Attribution {
delegated_attribution(&self.principal, &self.delegation_chain)
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct RequestContext {
pub request_id: RequestId,
pub principal: Option<Principal>,
pub delegation_chain: Vec<DelegationStep>,
pub traceparent: Option<TraceParent>,
pub idempotency_key: Option<IdempotencyKey>,
pub started_at: DateTime<Utc>,
}
impl RequestContext {
#[must_use]
pub fn new(request_id: RequestId, started_at: DateTime<Utc>) -> Self {
Self {
request_id,
principal: None,
delegation_chain: Vec::new(),
traceparent: None,
idempotency_key: None,
started_at,
}
}
#[must_use]
pub fn principal(&self) -> Option<&Principal> {
self.principal.as_ref()
}
#[must_use]
pub fn is_delegated(&self) -> bool {
!self.delegation_chain.is_empty()
}
}
impl fmt::Debug for RequestContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestContext")
.field("request_id", &self.request_id)
.field(
"principal",
&self.principal.as_ref().map(|principal| principal.id),
)
.field("delegation_chain_depth", &self.delegation_chain.len())
.field("traceparent", &self.traceparent)
.field(
"idempotency_key",
&self.idempotency_key.as_ref().map(|_| "<redacted>"),
)
.field("started_at", &self.started_at)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct TraceParent(String);
impl TraceParent {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::{AcceptedAuthority, DelegationStep, RequestContext, TraceParent};
use crate::permission::PermissionSet;
use crate::principal::{Principal, PrincipalId, PrincipalKind, PrincipalRef};
use chrono::Utc;
use wyrd_semver::VersionBlock;
use wyrd_spec::envelope::CardKind;
use wyrd_spec::ids::{CardName, IdempotencyKey, SpaceName};
use wyrd_spec::reference::{CardRef, CardRefScope};
use wyrd_spec::request_id::RequestId;
fn request_id() -> RequestId {
RequestId::parse(&uuid::Uuid::now_v7().to_string())
.expect("generated UUIDv7 is a valid request id")
}
fn card_ref(kind: CardKind, name: &str) -> CardRef {
CardRef {
kind,
name: CardName::new(name).expect("static card name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("prod").expect("static space is valid")),
uid: None,
}
}
fn service_principal(name: &str) -> Principal {
let card_ref = card_ref(CardKind::Service, name);
Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::Service {
card_ref: Some(card_ref.clone()),
card_ref_scope: CardRefScope::own(&card_ref),
},
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: Vec::new(),
effective_permissions: PermissionSet::new(),
credential_id: None,
}
}
#[test]
fn delegation_chain_default_empty() {
let context = RequestContext::new(request_id(), Utc::now());
assert!(context.delegation_chain.is_empty());
assert!(!context.is_delegated());
}
#[test]
fn delegation_step_carries_self_describing_projection() {
let principal = service_principal("billing");
let step = DelegationStep {
principal: PrincipalRef::from_principal(&principal),
};
assert_eq!(step.principal.id, principal.id);
assert_eq!(step.principal.kind, principal.kind);
assert!(step.principal.card_ref().is_some());
}
#[test]
fn delegation_chain_round_trips() {
let mut context = RequestContext::new(request_id(), Utc::now());
context.principal = Some(service_principal("current"));
context.traceparent = Some(TraceParent::new(
"00-0123456789abcdef0123456789abcdef-0123456789abcdef-01",
));
context.delegation_chain = vec![
DelegationStep {
principal: PrincipalRef::from_principal(&service_principal("first")),
},
DelegationStep {
principal: PrincipalRef::from_principal(&service_principal("second")),
},
];
let json = serde_json::to_string(&context).expect("serialize request context");
let parsed: RequestContext =
serde_json::from_str(&json).expect("deserialize request context");
assert_eq!(parsed.delegation_chain, context.delegation_chain);
assert_eq!(parsed.traceparent, context.traceparent);
assert!(parsed.is_delegated());
}
#[test]
fn debug_redacts_idempotency_key() {
let mut context = RequestContext::new(request_id(), Utc::now());
context.idempotency_key =
Some(IdempotencyKey::new("client-private-retry-key").expect("valid opaque key"));
let debug = format!("{context:?}");
assert!(debug.contains(r#"idempotency_key: Some("<redacted>")"#));
assert!(!debug.contains("client-private-retry-key"));
}
#[test]
fn debug_redacts_delegation_chain_to_depth() {
let mut context = RequestContext::new(request_id(), Utc::now());
context.delegation_chain = vec![
DelegationStep {
principal: PrincipalRef::from_principal(&service_principal("first")),
},
DelegationStep {
principal: PrincipalRef::from_principal(&service_principal("second")),
},
DelegationStep {
principal: PrincipalRef::from_principal(&service_principal("third")),
},
];
let debug = format!("{context:?}");
assert!(debug.contains("delegation_chain_depth: 3"));
assert!(!debug.contains("first"));
assert!(!debug.contains("second"));
assert!(!debug.contains("third"));
}
#[test]
fn accepted_authority_round_trips_without_credential() {
let mut subject = service_principal("subject");
subject.credential_id = Some(uuid::Uuid::now_v7());
let actor = service_principal("actor");
let chain = vec![DelegationStep {
principal: PrincipalRef::from_principal(&actor),
}];
let accepted = AcceptedAuthority::new(&subject, &chain);
let json = serde_json::to_value(&accepted).expect("serialize accepted authority");
let parsed: AcceptedAuthority =
serde_json::from_value(json).expect("deserialize accepted authority");
assert_eq!(parsed, accepted);
assert_eq!(parsed.principal.credential_id, None);
assert_eq!(parsed.delegation_chain, chain);
assert_eq!(parsed.attribution().sub_principal_id, subject.id);
assert_eq!(parsed.attribution().act_principal_id, actor.id);
assert_eq!(
AcceptedAuthority::new(&subject, &[])
.attribution()
.act_principal_id,
subject.id
);
}
}