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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
use std::sync::Arc;
use tracing::warn;
use uuid::Uuid;
use sqlx::SqlitePool;
use crate::errors::AppError;
/// Audit log severity levels for compliance filtering and alerting.
/// Used to prioritize audit events for security monitoring.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AuditSeverity {
Info, // Standard operations (info-level events)
Warn, // Security-relevant operations (potential threats)
Critical, // Security incidents and critical changes
}
impl AuditSeverity {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Critical => "CRITICAL",
}
}
}
/// Action types for audit logging conform to ISO 15189, RGPD, and SOX.
///
/// Using dot-namespaced action names allows efficient filtering by subsystem:
/// - `bootstrap.*` — first-admin initialization
/// - `auth.*` — authentication flows (login/logout)
/// - `rbac.*` — role-based access control changes (CRITICAL)
/// - `secret.*` — secret CRUD operations
/// - `license.*` — license validation
#[derive(Clone, Debug)]
pub enum AuditAction {
BootstrapInit, // First admin account creation
BootstrapInitSuccess, // Bootstrap succeeded
BootstrapInitFailure, // Bootstrap failed
AuthLogin, // Login attempt
AuthLoginSuccess, // Login succeeded
AuthLoginFailure, // Login failed (WARN severity)
AuthLogout, // Logout
RbacPermissionChange, // RBAC permission modified (CRITICAL severity)
RbacRoleAssignment, // Role assigned to user (CRITICAL severity)
RbacRoleRevocation, // Role revoked from user (CRITICAL severity)
SecretView, // Secret viewed (INFO severity)
SecretCreate, // Secret created
SecretUpdate, // Secret updated
SecretDelete, // Secret deleted (WARN severity)
LicenseCheck, // License validation at startup
LicenseCheckSuccess, // License valid
LicenseCheckFailure, // License invalid
}
impl AuditAction {
pub fn as_str(&self) -> &'static str {
match self {
Self::BootstrapInit => "bootstrap.init",
Self::BootstrapInitSuccess => "bootstrap.init.success",
Self::BootstrapInitFailure => "bootstrap.init.failure",
Self::AuthLogin => "auth.login",
Self::AuthLoginSuccess => "auth.login.success",
Self::AuthLoginFailure => "auth.login.failure",
Self::AuthLogout => "auth.logout",
Self::RbacPermissionChange => "rbac.permission.change",
Self::RbacRoleAssignment => "rbac.role.assignment",
Self::RbacRoleRevocation => "rbac.role.revocation",
Self::SecretView => "secret.view",
Self::SecretCreate => "secret.create",
Self::SecretUpdate => "secret.update",
Self::SecretDelete => "secret.delete",
Self::LicenseCheck => "license.check",
Self::LicenseCheckSuccess => "license.check.success",
Self::LicenseCheckFailure => "license.check.failure",
}
}
/// Returns the recommended severity level for this action.
/// Used for audit log filtering and security alerting.
pub fn severity(&self) -> AuditSeverity {
match self {
// CRITICAL: Security incidents and critical permission changes
Self::RbacPermissionChange
| Self::RbacRoleAssignment
| Self::RbacRoleRevocation
| Self::AuthLoginFailure
| Self::BootstrapInitFailure => AuditSeverity::Critical,
// WARN: Security-relevant operations (deletions, failures)
Self::SecretDelete | Self::AuthLogout => AuditSeverity::Warn,
// INFO: Standard operations
_ => AuditSeverity::Info,
}
}
}
/// High-performance, non-blocking audit service for compliance logging.
///
/// All operations are spawned to background tasks to prevent UI blocking.
/// Designed for ISO 15189, RGPD, SOX, and medical-lab compliance.
#[derive(Clone)]
pub struct AuditService {
db_pool: Arc<SqlitePool>,
}
impl AuditService {
pub fn new(db_pool: SqlitePool) -> Self {
Self {
db_pool: Arc::new(db_pool),
}
}
/// Log an audit event asynchronously (non-blocking).
///
/// This spawns a background task and completes immediately.
/// The actual database write happens out-of-band.
pub fn log_async(
&self,
actor_user_id: Option<Uuid>,
action: AuditAction,
target_type: Option<&str>,
target_id: Option<&str>,
detail: Option<&str>,
) {
let pool = Arc::clone(&self.db_pool);
let actor_id_str = actor_user_id.map(|id| id.to_string());
let target_type_str = target_type.map(|s| s.to_string());
let target_id_str = target_id.map(|s| s.to_string());
let detail_str = detail.map(|s| s.to_string());
let action_str = action.as_str().to_string();
// Spawn background task — non-blocking.
tokio::spawn(async move {
if let Err(e) = sqlx::query(
r#"
INSERT INTO audit_log (actor_user_id, action, target_type, target_id, detail)
VALUES (?1, ?2, ?3, ?4, ?5)
"#,
)
.bind(actor_id_str)
.bind(action_str)
.bind(target_type_str)
.bind(target_id_str)
.bind(detail_str)
.execute(pool.as_ref())
.await
{
warn!("audit log write failed: {:?}", e);
}
});
}
/// Log an audit event synchronously (blocking, returns Result).
///
/// Used for critical operations that must complete before proceeding
/// (e.g., bootstrap completion verification).
pub async fn log_sync(
&self,
actor_user_id: Option<Uuid>,
action: AuditAction,
target_type: Option<&str>,
target_id: Option<&str>,
detail: Option<&str>,
) -> Result<(), AppError> {
let actor_id_str = actor_user_id.map(|id| id.to_string());
let target_type_str = target_type.map(|s| s.to_string());
let target_id_str = target_id.map(|s| s.to_string());
let detail_str = detail.map(|s| s.to_string());
let action_str = action.as_str();
sqlx::query(
r#"
INSERT INTO audit_log (actor_user_id, action, target_type, target_id, detail)
VALUES (?1, ?2, ?3, ?4, ?5)
"#,
)
.bind(actor_id_str)
.bind(action_str)
.bind(target_type_str)
.bind(target_id_str)
.bind(detail_str)
.execute(self.db_pool.as_ref())
.await?;
Ok(())
}
/// Convenience: log bootstrap success.
pub fn log_bootstrap_success(&self, username: &str) {
self.log_async(
None, // No user ID yet; bootstrap happens before admin exists
AuditAction::BootstrapInitSuccess,
Some("user"),
Some(username),
Some("First admin account initialized"),
);
}
/// Convenience: log bootstrap failure.
pub fn log_bootstrap_failure(&self, reason: &str) {
self.log_async(
None,
AuditAction::BootstrapInitFailure,
Some("bootstrap"),
None,
Some(reason),
);
}
/// Convenience: log login success.
pub fn log_login_success(&self, user_id: Uuid, username: &str) {
self.log_async(
Some(user_id),
AuditAction::AuthLoginSuccess,
Some("user"),
Some(username),
Some(&format!("Login successful for user {}", username)),
);
}
/// Convenience: log login failure.
pub fn log_login_failure(&self, username: &str, reason: &str) {
self.log_async(
None, // Actor not yet identified on failed login
AuditAction::AuthLoginFailure,
Some("user"),
Some(username),
Some(&format!("Login failed: {}", reason)),
);
}
/// Convenience: log secret view.
pub fn log_secret_view(&self, user_id: Uuid, secret_id: Uuid, secret_title: &str) {
self.log_async(
Some(user_id),
AuditAction::SecretView,
Some("secret"),
Some(&secret_id.to_string()),
Some(&format!("Viewed secret: {}", secret_title)),
);
}
/// Convenience: log license check.
pub fn log_license_check_success(&self) {
self.log_async(
None,
AuditAction::LicenseCheckSuccess,
Some("license"),
None,
Some("License validation passed at startup"),
);
}
/// Convenience: log license check failure.
pub fn log_license_check_failure(&self, reason: &str) {
self.log_async(
None,
AuditAction::LicenseCheckFailure,
Some("license"),
None,
Some(reason),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_action_strings() {
assert_eq!(
AuditAction::BootstrapInitSuccess.as_str(),
"bootstrap.init.success"
);
assert_eq!(AuditAction::AuthLoginSuccess.as_str(), "auth.login.success");
assert_eq!(AuditAction::SecretView.as_str(), "secret.view");
}
}