Skip to main content

session_guard/
event.rs

1//! Security events: what happened to a session, recorded in the store and passed to the hook.
2
3use std::{fmt, net::IpAddr};
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::{checks::Check, subject::Subject};
10
11/// What happened.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum EventKind {
16    /// A session was created.
17    Login,
18    /// The client ended its session.
19    Logout,
20    /// The risk score reached the step-up threshold.
21    StepUpRequired,
22    /// The subject re-verified and the session was trusted again.
23    StepUpCompleted,
24    /// The risk score reached the revoke threshold (or step-up under
25    /// [`StepUpPolicy::Revoke`](crate::StepUpPolicy::Revoke)).
26    RiskRevoked,
27    /// A superseded token was presented after its successor was used: two parties hold the
28    /// session, so it was revoked. Always enforced, also in shadow mode.
29    TokenReuse,
30    /// A token whose successor never reached the client was replaced (lost response).
31    TokenReissued,
32}
33
34impl EventKind {
35    /// Every kind, in declaration order.
36    pub const ALL: [EventKind; 7] = [
37        EventKind::Login,
38        EventKind::Logout,
39        EventKind::StepUpRequired,
40        EventKind::StepUpCompleted,
41        EventKind::RiskRevoked,
42        EventKind::TokenReuse,
43        EventKind::TokenReissued,
44    ];
45
46    /// Stable `snake_case` name, as stored.
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            EventKind::Login => "login",
50            EventKind::Logout => "logout",
51            EventKind::StepUpRequired => "step_up_required",
52            EventKind::StepUpCompleted => "step_up_completed",
53            EventKind::RiskRevoked => "risk_revoked",
54            EventKind::TokenReuse => "token_reuse",
55            EventKind::TokenReissued => "token_reissued",
56        }
57    }
58
59    /// Parses [`as_str`](Self::as_str) output.
60    pub fn parse(name: &str) -> Option<Self> {
61        Self::ALL.into_iter().find(|k| k.as_str() == name)
62    }
63
64    /// Whether this kind points at a possible attack (logged as a warning).
65    pub const fn is_security_relevant(self) -> bool {
66        matches!(
67            self,
68            EventKind::StepUpRequired | EventKind::RiskRevoked | EventKind::TokenReuse | EventKind::TokenReissued
69        )
70    }
71}
72
73impl fmt::Display for EventKind {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79/// One security event.
80///
81/// The subject is kept in its stored form (canonical JSON, see [`Subject`]); use
82/// [`subject`](Self::subject) to get it back as your type.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct SessionEvent {
85    /// The session concerned.
86    pub session_id: Uuid,
87    /// The session's subject, as stored.
88    pub subject: String,
89    /// What happened.
90    pub kind: EventKind,
91    /// `false` when the decision was only recorded (shadow mode, or
92    /// [`StepUpPolicy::Allow`](crate::StepUpPolicy::Allow)).
93    pub enforced: bool,
94    /// Risk score, for risk decisions.
95    pub score: Option<u32>,
96    /// Signals that fired, for risk decisions.
97    pub signals: Vec<Check>,
98    /// Client IP, or only its /24 or /48 network address with
99    /// [`minimize_event_ip`](crate::GuardConfigBuilder::minimize_event_ip).
100    pub ip: IpAddr,
101    /// Client User-Agent, truncated to the header cap.
102    pub user_agent: Option<String>,
103    /// When it happened.
104    pub at: DateTime<Utc>,
105}
106
107impl SessionEvent {
108    /// The subject as `S`, or `None` if the stored value does not parse as `S`.
109    pub fn subject<S: Subject>(&self) -> Option<S> {
110        crate::subject::decode(&self.subject)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn names_round_trip_and_match_serde() {
120        for kind in EventKind::ALL {
121            assert_eq!(EventKind::parse(kind.as_str()), Some(kind));
122            assert_eq!(serde_json::to_string(&kind).unwrap(), format!("\"{kind}\""));
123        }
124        assert_eq!(EventKind::parse("nope"), None);
125    }
126
127    #[test]
128    fn subject_decodes() {
129        let id = Uuid::new_v4();
130        let event = SessionEvent {
131            session_id: Uuid::new_v4(),
132            subject: crate::subject::encode(&id).unwrap(),
133            kind: EventKind::Login,
134            enforced: true,
135            score: None,
136            signals: vec![],
137            ip: "203.0.113.7".parse().unwrap(),
138            user_agent: None,
139            at: Utc::now(),
140        };
141        assert_eq!(event.subject::<Uuid>(), Some(id));
142        assert_eq!(event.subject::<i64>(), None);
143    }
144}