1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum EventKind {
16 Login,
18 Logout,
20 StepUpRequired,
22 StepUpCompleted,
24 RiskRevoked,
27 TokenReuse,
30 TokenReissued,
32}
33
34impl EventKind {
35 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 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 pub fn parse(name: &str) -> Option<Self> {
61 Self::ALL.into_iter().find(|k| k.as_str() == name)
62 }
63
64 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#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct SessionEvent {
85 pub session_id: Uuid,
87 pub subject: String,
89 pub kind: EventKind,
91 pub enforced: bool,
94 pub score: Option<u32>,
96 pub signals: Vec<Check>,
98 pub ip: IpAddr,
101 pub user_agent: Option<String>,
103 pub at: DateTime<Utc>,
105}
106
107impl SessionEvent {
108 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}