quantrs2_device/security/
audit.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[non_exhaustive]
15pub enum OperationType {
16 CircuitSubmit,
18 ResultFetch,
20 BackendQuery,
22 Authentication,
24 JobCancel,
26 CalibrationFetch,
28 StatusCheck,
30}
31
32impl std::fmt::Display for OperationType {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 let s = match self {
35 OperationType::CircuitSubmit => "CircuitSubmit",
36 OperationType::ResultFetch => "ResultFetch",
37 OperationType::BackendQuery => "BackendQuery",
38 OperationType::Authentication => "Authentication",
39 OperationType::JobCancel => "JobCancel",
40 OperationType::CalibrationFetch => "CalibrationFetch",
41 OperationType::StatusCheck => "StatusCheck",
42 };
43 f.write_str(s)
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AuditEvent {
50 pub id: String,
52 pub timestamp_ms: u64,
54 pub operation: OperationType,
56 pub backend_id: Option<String>,
58 pub circuit_hash: Option<String>,
60 pub user_id: Option<String>,
62 pub success: bool,
64 pub duration_ms: Option<u64>,
66 pub error: Option<String>,
68 pub metadata: HashMap<String, String>,
70}
71
72impl AuditEvent {
73 pub fn new(operation: OperationType, success: bool) -> Self {
75 let timestamp_ms = SystemTime::now()
76 .duration_since(UNIX_EPOCH)
77 .unwrap_or(Duration::ZERO)
78 .as_millis() as u64;
79
80 let id = format!("{}-{}", timestamp_ms, fastrand::u64(..));
81
82 Self {
83 id,
84 timestamp_ms,
85 operation,
86 backend_id: None,
87 circuit_hash: None,
88 user_id: None,
89 success,
90 duration_ms: None,
91 error: None,
92 metadata: HashMap::new(),
93 }
94 }
95
96 pub fn with_backend(mut self, backend: impl Into<String>) -> Self {
98 self.backend_id = Some(backend.into());
99 self
100 }
101
102 pub fn with_circuit_hash(mut self, hash: impl Into<String>) -> Self {
104 self.circuit_hash = Some(hash.into());
105 self
106 }
107
108 pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
110 self.user_id = Some(user_id.into());
111 self
112 }
113
114 pub fn with_duration_ms(mut self, ms: u64) -> Self {
116 self.duration_ms = Some(ms);
117 self
118 }
119
120 pub fn with_error(mut self, error: impl Into<String>) -> Self {
122 self.error = Some(error.into());
123 self
124 }
125
126 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
128 self.metadata.insert(key.into(), value.into());
129 self
130 }
131}
132
133pub fn circuit_hash(circuit: &str) -> String {
137 let mut h: u64 = 0xcbf29ce484222325; for byte in circuit.bytes() {
139 h ^= byte as u64;
140 h = h.wrapping_mul(0x100000001b3); }
142 format!("{:016x}", h)
143}
144
145#[derive(Debug)]
147#[non_exhaustive]
148pub enum AuditError {
149 IoError(std::io::Error),
151 SerializationError(String),
153}
154
155impl std::fmt::Display for AuditError {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 AuditError::IoError(e) => write!(f, "audit IO error: {}", e),
159 AuditError::SerializationError(s) => write!(f, "audit serialization error: {}", s),
160 }
161 }
162}
163
164impl std::error::Error for AuditError {
165 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166 match self {
167 AuditError::IoError(e) => Some(e),
168 AuditError::SerializationError(_) => None,
169 }
170 }
171}
172
173impl From<std::io::Error> for AuditError {
174 fn from(e: std::io::Error) -> Self {
175 AuditError::IoError(e)
176 }
177}
178
179pub trait AuditLogger: Send + Sync {
181 fn log(&self, event: AuditEvent) -> Result<(), AuditError>;
183 fn flush(&self) -> Result<(), AuditError>;
185 fn events(&self) -> Vec<AuditEvent> {
187 vec![]
188 }
189}
190
191pub struct FileAuditLogger {
193 path: PathBuf,
194 file: Arc<Mutex<std::fs::File>>,
195}
196
197impl FileAuditLogger {
198 pub fn new(path: impl Into<PathBuf>) -> Result<Self, AuditError> {
200 let path = path.into();
201 let file = std::fs::OpenOptions::new()
202 .create(true)
203 .append(true)
204 .open(&path)
205 .map_err(AuditError::IoError)?;
206 Ok(Self {
207 path,
208 file: Arc::new(Mutex::new(file)),
209 })
210 }
211
212 pub fn path(&self) -> &std::path::Path {
214 &self.path
215 }
216}
217
218impl AuditLogger for FileAuditLogger {
219 fn log(&self, event: AuditEvent) -> Result<(), AuditError> {
220 use std::io::Write;
221 let json = serde_json::to_string(&event)
222 .map_err(|e| AuditError::SerializationError(e.to_string()))?;
223 let mut f = self.file.lock().unwrap_or_else(|e| e.into_inner());
224 writeln!(f, "{}", json).map_err(AuditError::IoError)
225 }
226
227 fn flush(&self) -> Result<(), AuditError> {
228 use std::io::Write;
229 let mut f = self.file.lock().unwrap_or_else(|e| e.into_inner());
230 f.flush().map_err(AuditError::IoError)
231 }
232}
233
234#[derive(Default)]
236pub struct InMemoryAuditLogger {
237 stored: Arc<Mutex<Vec<AuditEvent>>>,
238}
239
240impl InMemoryAuditLogger {
241 pub fn new() -> Self {
243 Self::default()
244 }
245
246 pub fn event_count(&self) -> usize {
248 self.stored.lock().unwrap_or_else(|e| e.into_inner()).len()
249 }
250}
251
252impl AuditLogger for InMemoryAuditLogger {
253 fn log(&self, event: AuditEvent) -> Result<(), AuditError> {
254 self.stored
255 .lock()
256 .unwrap_or_else(|e| e.into_inner())
257 .push(event);
258 Ok(())
259 }
260
261 fn flush(&self) -> Result<(), AuditError> {
262 Ok(())
263 }
264
265 fn events(&self) -> Vec<AuditEvent> {
266 self.stored
267 .lock()
268 .unwrap_or_else(|e| e.into_inner())
269 .clone()
270 }
271}
272
273pub struct NullAuditLogger;
275
276impl AuditLogger for NullAuditLogger {
277 fn log(&self, _event: AuditEvent) -> Result<(), AuditError> {
278 Ok(())
279 }
280
281 fn flush(&self) -> Result<(), AuditError> {
282 Ok(())
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use std::env;
290
291 #[test]
292 fn test_in_memory_logger() {
293 let logger = InMemoryAuditLogger::new();
294 for _ in 0..100 {
295 let event = AuditEvent::new(OperationType::CircuitSubmit, true)
296 .with_backend("test_backend")
297 .with_duration_ms(42);
298 logger.log(event).expect("log should succeed");
299 }
300 assert_eq!(logger.event_count(), 100);
301 }
302
303 #[test]
304 fn test_audit_event_json_roundtrip() {
305 let event = AuditEvent::new(OperationType::ResultFetch, false)
306 .with_backend("ibm_nairobi")
307 .with_error("timeout");
308
309 let json = serde_json::to_string(&event).expect("serialization should succeed");
310 let parsed: AuditEvent =
311 serde_json::from_str(&json).expect("deserialization should succeed");
312 assert_eq!(parsed.backend_id, Some("ibm_nairobi".to_string()));
313 assert!(!parsed.success);
314 assert_eq!(parsed.error, Some("timeout".to_string()));
315 }
316
317 #[test]
318 fn test_in_memory_logger_events() {
319 let logger = InMemoryAuditLogger::new();
320 let event = AuditEvent::new(OperationType::BackendQuery, true)
321 .with_user("test-user")
322 .with_metadata("region", "us-east-1");
323 logger.log(event).expect("log should succeed");
324
325 let events = logger.events();
326 assert_eq!(events.len(), 1);
327 assert_eq!(events[0].user_id, Some("test-user".to_string()));
328 assert_eq!(
329 events[0].metadata.get("region"),
330 Some(&"us-east-1".to_string())
331 );
332 }
333
334 #[test]
335 fn test_file_audit_logger() {
336 let dir = env::temp_dir();
337 let path = dir.join(format!("quantrs_audit_test_{}.jsonl", fastrand::u64(..)));
338 let logger = FileAuditLogger::new(&path).expect("file logger creation should succeed");
339
340 let event = AuditEvent::new(OperationType::Authentication, true);
341 logger.log(event).expect("log should succeed");
342 logger.flush().expect("flush should succeed");
343
344 assert_eq!(logger.path(), path.as_path());
345
346 let content = std::fs::read_to_string(&path).expect("should read audit file");
347 assert!(content.contains("Authentication"));
348
349 let _ = std::fs::remove_file(&path);
350 }
351
352 #[test]
353 fn test_file_audit_logger_multiple_entries() {
354 let dir = env::temp_dir();
355 let path = dir.join(format!("quantrs_audit_multi_{}.jsonl", fastrand::u64(..)));
356 let logger = FileAuditLogger::new(&path).expect("file logger creation should succeed");
357
358 for op in [
359 OperationType::CircuitSubmit,
360 OperationType::ResultFetch,
361 OperationType::JobCancel,
362 ] {
363 let event = AuditEvent::new(op, true);
364 logger.log(event).expect("log should succeed");
365 }
366 logger.flush().expect("flush should succeed");
367
368 let content = std::fs::read_to_string(&path).expect("should read audit file");
369 let lines: Vec<&str> = content.lines().collect();
370 assert_eq!(lines.len(), 3);
371
372 let _ = std::fs::remove_file(&path);
373 }
374
375 #[test]
376 fn test_null_logger() {
377 let logger = NullAuditLogger;
378 let event = AuditEvent::new(OperationType::StatusCheck, true);
379 logger
380 .log(event)
381 .expect("null logger should always succeed");
382 logger.flush().expect("null flush should always succeed");
383 assert!(logger.events().is_empty());
384 }
385
386 #[test]
387 fn test_circuit_hash_deterministic() {
388 let circuit = "H q[0]; CNOT q[0],q[1];";
389 let h1 = circuit_hash(circuit);
390 let h2 = circuit_hash(circuit);
391 assert_eq!(h1, h2);
392 assert_eq!(h1.len(), 16);
393 }
394
395 #[test]
396 fn test_circuit_hash_different_for_different_inputs() {
397 let h1 = circuit_hash("H q[0];");
398 let h2 = circuit_hash("X q[0];");
399 assert_ne!(h1, h2);
400 }
401
402 #[test]
403 fn test_operation_type_display() {
404 assert_eq!(OperationType::CircuitSubmit.to_string(), "CircuitSubmit");
405 assert_eq!(OperationType::Authentication.to_string(), "Authentication");
406 }
407
408 #[test]
409 fn test_audit_event_builder_chain() {
410 let event = AuditEvent::new(OperationType::CalibrationFetch, false)
411 .with_backend("ibm_lagos")
412 .with_circuit_hash("abcdef0123456789")
413 .with_user("service-account")
414 .with_duration_ms(250)
415 .with_error("backend unavailable")
416 .with_metadata("attempt", "3")
417 .with_metadata("region", "eu-west");
418
419 assert_eq!(event.backend_id, Some("ibm_lagos".to_string()));
420 assert_eq!(event.circuit_hash, Some("abcdef0123456789".to_string()));
421 assert_eq!(event.user_id, Some("service-account".to_string()));
422 assert_eq!(event.duration_ms, Some(250));
423 assert!(!event.success);
424 assert_eq!(event.metadata.get("attempt"), Some(&"3".to_string()));
425 }
426}