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