Skip to main content

rustauth_scim/
audit.rs

1//! Optional SCIM audit hooks (structured logging + integrator callback).
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use rustauth_core::context::AuthContext;
8
9use crate::options::{ScimAuditEvent, ScimAuditEventKind, ScimAuditSeverity, ScimOptions};
10
11type AuditEventFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
12
13/// Async sink for SCIM audit events.
14#[derive(Clone)]
15pub struct ScimAuditEventResolver {
16    resolver: Arc<dyn Fn(ScimAuditEvent) -> AuditEventFuture + Send + Sync>,
17}
18
19impl ScimAuditEventResolver {
20    /// Create an audit event sink from an async function.
21    pub fn new<F, Fut>(resolver: F) -> Self
22    where
23        F: Fn(ScimAuditEvent) -> Fut + Send + Sync + 'static,
24        Fut: Future<Output = ()> + Send + 'static,
25    {
26        Self {
27            resolver: Arc::new(move |event| Box::pin(resolver(event))),
28        }
29    }
30
31    /// Emit an audit event.
32    pub async fn resolve(&self, event: ScimAuditEvent) {
33        (self.resolver)(event).await;
34    }
35}
36
37impl std::fmt::Debug for ScimAuditEventResolver {
38    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        formatter.write_str("ScimAuditEventResolver(..)")
40    }
41}
42
43impl PartialEq for ScimAuditEventResolver {
44    fn eq(&self, _other: &Self) -> bool {
45        false
46    }
47}
48
49pub(crate) async fn emit(context: &AuthContext, options: &ScimOptions, event: ScimAuditEvent) {
50    log(context, &event);
51    if let Some(resolver) = &options.audit_event {
52        resolver.resolve(event).await;
53    }
54}
55
56fn log(context: &AuthContext, event: &ScimAuditEvent) {
57    let message = match event.kind {
58        ScimAuditEventKind::TokenGenerated => "scim token generated",
59        ScimAuditEventKind::UserProvisioned => "scim user provisioned",
60        ScimAuditEventKind::UserDeprovisioned => "scim user deprovisioned",
61        ScimAuditEventKind::BulkFailed => "scim bulk operation failed",
62        ScimAuditEventKind::BulkRolledBack => "scim atomic bulk rolled back",
63    };
64    let mut args = Vec::new();
65    if let Some(provider_id) = event.provider_id.as_deref() {
66        args.push(provider_id);
67    }
68    if let Some(user_id) = event.user_id.as_deref() {
69        args.push(user_id);
70    }
71    if let Some(organization_id) = event.organization_id.as_deref() {
72        args.push(organization_id);
73    }
74    if let Some(reason) = event.reason.as_deref() {
75        args.push(reason);
76    }
77    match event.severity {
78        ScimAuditSeverity::Info => context.logger.info(message, &args),
79        ScimAuditSeverity::Warn => context.logger.warn(message, &args),
80        ScimAuditSeverity::Error => context.logger.error(message, &args),
81    }
82}