use auths_policy::decision::{Decision, ReasonCode};
use auths_verifier::IdentityDID;
use chrono::{DateTime, Utc};
use super::types::{IdpAttestation, IdpId, LifecycleClaim};
#[derive(Debug, Clone, PartialEq)]
pub struct FederationSignal {
pub subject: IdentityDID,
pub idp: IdpId,
pub claim: LifecycleClaim,
pub fresh: bool,
}
impl IdpAttestation {
pub fn as_signal(&self, now: DateTime<Utc>) -> FederationSignal {
FederationSignal {
subject: self.content.subject.clone(),
idp: self.content.idp.clone(),
claim: self.content.claim.clone(),
fresh: self.content.expires_at > now,
}
}
}
pub fn evaluate_idp_signals(signals: &[FederationSignal]) -> Decision {
if let Some(deny) = signals.iter().find(|s| {
s.fresh
&& matches!(
s.claim,
LifecycleClaim::Suspended | LifecycleClaim::Terminated
)
}) {
return Decision::deny(
ReasonCode::Revoked,
format!(
"IdP '{}' attests subject '{}' is no longer active",
deny.idp.as_str(),
deny.subject.as_str()
),
);
}
let has_active = signals.iter().any(|s| {
s.fresh
&& matches!(
s.claim,
LifecycleClaim::Employed | LifecycleClaim::GroupMember(_)
)
});
if has_active {
Decision::indeterminate(
ReasonCode::AttrMismatch,
"IdP attests an active lifecycle signal; this is corroborating evidence, \
not an authority grant",
)
} else {
Decision::indeterminate(
ReasonCode::MissingField,
"no fresh IdP lifecycle signal for this subject",
)
}
}