1use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, krabka_macros::EnumStr)]
11#[enum_str(case = "lowercase")]
12pub enum AuditOutcome {
13 Success,
14 Failure,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct AuditPrincipal {
20 pub name: String,
21 pub auth_method: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct AuditEndpoint {
27 pub ip: String,
28 pub port: u16,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33pub struct AuditResource {
34 pub resource_type: String,
35 pub name: String,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40pub enum LifecycleKind {
41 BrokerStarted,
42 BrokerStopping,
43 ConfigApplied,
44 TlsReloaded,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, krabka_macros::EnumStr)]
56#[enum_str(case = "lowercase", as_str = as_name, parse = from_name)]
57pub enum PrivilegedPhase {
58 Attempted,
59 Proposed,
60 Approved,
61 Consumed,
62 Applied,
63 Refused,
64 Bypassed,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, krabka_macros::EnumStr)]
72#[enum_str(case = "snake_case", as_str = as_header, parse = from_header)]
73pub enum AuditEventClass {
74 Authentication,
75 Authorization,
76 ApiActivity,
77 ApplicationLifecycle,
78 Checkpoint,
80 RecordsLost,
82}
83
84impl AuditEventClass {
85 #[must_use]
87 pub fn tag(self) -> u8 {
88 match self {
89 Self::Authentication => 0,
90 Self::Authorization => 1,
91 Self::ApiActivity => 2,
92 Self::ApplicationLifecycle => 3,
93 Self::Checkpoint => 4,
94 Self::RecordsLost => 5,
95 }
96 }
97
98 #[must_use]
100 pub fn from_tag(tag: u8) -> Option<Self> {
101 match tag {
102 0 => Some(Self::Authentication),
103 1 => Some(Self::Authorization),
104 2 => Some(Self::ApiActivity),
105 3 => Some(Self::ApplicationLifecycle),
106 4 => Some(Self::Checkpoint),
107 5 => Some(Self::RecordsLost),
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum AuditEvent {
119 Authentication {
120 outcome: AuditOutcome,
121 mechanism: String,
122 principal: AuditPrincipal,
123 source: AuditEndpoint,
124 reason: Option<String>,
125 time_ms: i64,
126 },
127 AuthorizationDenied {
128 principal: AuditPrincipal,
129 source: AuditEndpoint,
130 resource_type: String,
131 resource_name: String,
132 operation: String,
133 time_ms: i64,
134 },
135 AdminOperation {
136 outcome: AuditOutcome,
137 principal: AuditPrincipal,
138 source: AuditEndpoint,
139 operation: String,
140 resources: Vec<AuditResource>,
141 time_ms: i64,
142 },
143 PrivilegedAction {
164 outcome: AuditOutcome,
165 phase: PrivilegedPhase,
166 action: String,
167 target: String,
168 proposal_id: String,
169 principal: AuditPrincipal,
170 counterparties: Vec<AuditPrincipal>,
171 approver_set_fingerprint: String,
172 key_id: String,
173 signature: Vec<u8>,
174 signature_verified: bool,
175 signed_at_ms: i64,
188 source: AuditEndpoint,
189 reason: String,
190 time_ms: i64,
191 },
192 Lifecycle {
193 kind: LifecycleKind,
194 node_id: i64,
195 time_ms: i64,
196 },
197}
198
199impl AuditEvent {
200 #[must_use]
202 pub fn class(&self) -> AuditEventClass {
203 match self {
204 Self::Authentication { .. } => AuditEventClass::Authentication,
205 Self::AuthorizationDenied { .. } => AuditEventClass::Authorization,
206 Self::AdminOperation { .. } | Self::PrivilegedAction { .. } => {
207 AuditEventClass::ApiActivity
208 }
209 Self::Lifecycle { .. } => AuditEventClass::ApplicationLifecycle,
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use {
217 super::*,
218 crate::test_support::{admin, authentication, denied, privileged_freeze},
219 assert2::check,
220 };
221
222 #[yare::parameterized(
223 authentication = { AuditEventClass::Authentication },
224 authorization = { AuditEventClass::Authorization },
225 api_activity = { AuditEventClass::ApiActivity },
226 application_lifecycle = { AuditEventClass::ApplicationLifecycle },
227 checkpoint = { AuditEventClass::Checkpoint },
228 records_lost = { AuditEventClass::RecordsLost },
229 )]
230 fn class_tag_round_trips_and_header_maps(class: AuditEventClass) {
231 check!(AuditEventClass::from_tag(class.tag()) == Some(class));
232 check!(AuditEventClass::from_header(class.as_header()) == Some(class));
233 }
234
235 #[test]
236 fn class_tag_and_header_reject_unknown_values() {
237 check!(AuditEventClass::from_tag(99) == None);
238 check!(AuditEventClass::from_header("nope") == None);
239 }
240
241 #[test]
242 fn event_class_maps_each_variant() {
243 let authn = authentication(51120, Some("authentication failed"), 1_700_000_000_000);
244 let denied = denied(1);
245 let admin = admin(2);
246 let privileged = privileged_freeze(vec![0xde, 0xad], 3, 4);
247 let life = AuditEvent::Lifecycle {
248 kind: LifecycleKind::BrokerStarted,
249 node_id: 1,
250 time_ms: 3,
251 };
252 check!(
253 (
254 authn.class(),
255 denied.class(),
256 admin.class(),
257 privileged.class(),
258 life.class()
259 ) == (
260 AuditEventClass::Authentication,
261 AuditEventClass::Authorization,
262 AuditEventClass::ApiActivity,
263 AuditEventClass::ApiActivity,
264 AuditEventClass::ApplicationLifecycle,
265 )
266 );
267 }
268
269 #[yare::parameterized(
270 attempted = { PrivilegedPhase::Attempted, "attempted" },
271 proposed = { PrivilegedPhase::Proposed, "proposed" },
272 approved = { PrivilegedPhase::Approved, "approved" },
273 consumed = { PrivilegedPhase::Consumed, "consumed" },
274 applied = { PrivilegedPhase::Applied, "applied" },
275 refused = { PrivilegedPhase::Refused, "refused" },
276 bypassed = { PrivilegedPhase::Bypassed, "bypassed" },
277 )]
278 fn privileged_phase_name_round_trips(phase: PrivilegedPhase, name: &str) {
279 check!(phase.as_name() == name);
280 check!(PrivilegedPhase::from_name(phase.as_name()) == Some(phase));
281 }
282
283 #[test]
284 fn privileged_phase_rejects_unknown_name() {
285 check!(PrivilegedPhase::from_name("nope") == None);
286 }
287}