use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use arcly_http_core::observability::audit::{AuditOutcome, AuditPipeline, AuditRecord};
use crate::federation::{FederationAudit, FederationDecision, FederationEvent};
pub struct AuditPipelineFederationSink {
pipeline: Arc<AuditPipeline>,
}
impl AuditPipelineFederationSink {
pub fn new(pipeline: Arc<AuditPipeline>) -> Self {
Self { pipeline }
}
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn map_decision(decision: FederationDecision) -> (&'static str, AuditOutcome) {
match decision {
FederationDecision::ReturningLogin => ("federation.login", AuditOutcome::Success),
FederationDecision::Provisioned => ("federation.provision", AuditOutcome::Success),
FederationDecision::AutoLinked => ("federation.link", AuditOutcome::Success),
FederationDecision::LinkRequired => ("federation.link_required", AuditOutcome::Denied),
FederationDecision::Denied => ("federation.denied", AuditOutcome::Denied),
}
}
#[async_trait]
impl FederationAudit for AuditPipelineFederationSink {
async fn record(&self, event: FederationEvent) {
let (action, outcome) = map_decision(event.decision);
let status = match outcome {
AuditOutcome::Success => 200,
AuditOutcome::Denied => 403,
AuditOutcome::Error => 500,
};
self.pipeline.record(AuditRecord {
timestamp_ms: now_ms(),
action,
resource: "identity",
actor_sub: event.user_id.clone(),
actor_role: None,
tenant: event.tenant.clone(),
trace_id: event.trace_id.clone().unwrap_or_default(),
method: "POST".to_string(),
path: format!("/federated/{}/{}", event.provider, event.provider_id),
outcome,
status,
prev_hash: String::new(), });
}
}