Skip to main content

systemprompt_users/services/
sso_events.rs

1//! [`PostgresAuthEventSink`]: the core audit sink every SSO driver fans out
2//! to. One `sso_login_events` row per attempt; a write failure is logged and
3//! never fails the sign-in it records.
4//!
5//! Copyright (c) systemprompt.io — Business Source License 1.1.
6//! See <https://systemprompt.io> for licensing details.
7
8use async_trait::async_trait;
9use systemprompt_traits::sso::{AuthEventSink, SsoLoginRefused, SsoLoginSuccess};
10
11use crate::repository::{NewSsoLoginEvent, SsoLoginEventRepository};
12
13pub const OUTCOME_SUCCESS: &str = "success";
14pub const OUTCOME_REFUSED: &str = "refused";
15
16#[derive(Debug, Clone)]
17pub struct PostgresAuthEventSink {
18    repo: SsoLoginEventRepository,
19}
20
21impl PostgresAuthEventSink {
22    pub const fn new(repo: SsoLoginEventRepository) -> Self {
23        Self { repo }
24    }
25
26    async fn write(&self, event: NewSsoLoginEvent<'_>) {
27        if let Err(e) = self.repo.insert_sso_login_event(&event).await {
28            tracing::error!(
29                provider_id = event.provider_id,
30                outcome = event.outcome,
31                error = %e,
32                "failed to record SSO login event"
33            );
34        }
35    }
36}
37
38#[async_trait]
39impl AuthEventSink for PostgresAuthEventSink {
40    async fn on_login_success(&self, event: &SsoLoginSuccess<'_>) {
41        let groups_count = i32::try_from(event.identity.groups.len()).unwrap_or(i32::MAX);
42        self.write(NewSsoLoginEvent {
43            provider_id: event.provider_id,
44            issuer: event.issuer,
45            outcome: OUTCOME_SUCCESS,
46            reason: None,
47            user_id: Some(event.user_id),
48            email: Some(event.identity.email.as_str()),
49            external_sub: Some(event.identity.external_sub.as_str()),
50            ip: event.ip,
51            groups_count: Some(groups_count),
52        })
53        .await;
54    }
55
56    async fn on_login_refused(&self, event: &SsoLoginRefused<'_>) {
57        self.write(NewSsoLoginEvent {
58            provider_id: event.provider_id,
59            issuer: event.issuer,
60            outcome: OUTCOME_REFUSED,
61            reason: Some(event.reason),
62            user_id: None,
63            email: event.email,
64            external_sub: event.external_sub,
65            ip: event.ip,
66            groups_count: None,
67        })
68        .await;
69    }
70}