1use crate::AuthBackend;
4use async_trait::async_trait;
5use rusmes_proto::Username;
6use sqlx::{AnyPool, Row};
7use std::net::IpAddr;
8
9#[derive(Debug, Clone)]
11pub struct AuditLog {
12 pub username: String,
14 pub ip_address: Option<String>,
16 pub success: bool,
18 pub failure_reason: Option<String>,
20 pub timestamp: String,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum HashType {
27 Bcrypt,
29 Argon2,
31 ScramSha256,
33}
34
35impl HashType {
36 fn from_prefix(hash: &str) -> Self {
37 if hash.starts_with("$2") {
38 HashType::Bcrypt
39 } else if hash.starts_with("$argon2") {
40 HashType::Argon2
41 } else if hash.starts_with("$scram-sha-256$") {
42 HashType::ScramSha256
43 } else {
44 HashType::Bcrypt }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct UserMetadata {
52 pub enabled: bool,
54 pub quota_bytes: i64,
56 pub roles: Option<String>,
58}
59
60impl UserMetadata {
61 pub fn roles_vec(&self) -> Vec<String> {
63 self.roles
64 .as_ref()
65 .map(|r| r.split(',').map(|s| s.trim().to_string()).collect())
66 .unwrap_or_default()
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct SqlConfig {
73 pub database_url: String,
75 pub password_query: String,
77 pub list_users_query: String,
79 pub create_user_query: String,
81 pub delete_user_query: String,
83 pub update_password_query: String,
85 pub scram_params_query: Option<String>,
87 pub scram_stored_key_query: Option<String>,
89 pub scram_server_key_query: Option<String>,
91 pub store_scram_query: Option<String>,
93 pub audit_table: Option<String>,
95 pub max_connections: u32,
97}
98
99impl Default for SqlConfig {
100 fn default() -> Self {
101 Self {
102 database_url: "sqlite:file::memory:?cache=shared".to_string(),
103 password_query: "SELECT password_hash, enabled, quota_bytes, roles FROM users WHERE username = ?".to_string(),
104 list_users_query: "SELECT username FROM users".to_string(),
105 create_user_query: "INSERT INTO users (username, password_hash, enabled, quota_bytes, roles) VALUES (?, ?, 1, 1073741824, ?)".to_string(),
106 delete_user_query: "DELETE FROM users WHERE username = ?".to_string(),
107 update_password_query: "UPDATE users SET password_hash = ? WHERE username = ?".to_string(),
108 scram_params_query: Some("SELECT scram_salt, scram_iterations FROM users WHERE username = ?".to_string()),
109 scram_stored_key_query: Some("SELECT scram_stored_key FROM users WHERE username = ?".to_string()),
110 scram_server_key_query: Some("SELECT scram_server_key FROM users WHERE username = ?".to_string()),
111 store_scram_query: Some("UPDATE users SET scram_salt = ?, scram_iterations = ?, scram_stored_key = ?, scram_server_key = ? WHERE username = ?".to_string()),
112 audit_table: Some("auth_audit".to_string()),
113 max_connections: 10,
114 }
115 }
116}
117
118pub struct SqlBackend {
120 pool: AnyPool,
121 config: SqlConfig,
122}
123
124impl SqlBackend {
125 pub async fn new(config: SqlConfig) -> anyhow::Result<Self> {
127 sqlx::any::install_default_drivers();
129
130 let pool = sqlx::any::AnyPoolOptions::new()
131 .max_connections(config.max_connections)
132 .connect(&config.database_url)
133 .await?;
134
135 Ok(Self { pool, config })
136 }
137
138 pub async fn init_schema(&self) -> anyhow::Result<()> {
140 sqlx::query(
142 r#"
143 CREATE TABLE IF NOT EXISTS users (
144 id INTEGER PRIMARY KEY AUTOINCREMENT,
145 username TEXT UNIQUE NOT NULL,
146 password_hash TEXT NOT NULL,
147 enabled INTEGER NOT NULL DEFAULT 1,
148 quota_bytes BIGINT NOT NULL DEFAULT 1073741824,
149 roles TEXT,
150 scram_salt BLOB,
151 scram_iterations INTEGER,
152 scram_stored_key BLOB,
153 scram_server_key BLOB,
154 created_at TEXT DEFAULT CURRENT_TIMESTAMP,
155 updated_at TEXT DEFAULT CURRENT_TIMESTAMP
156 )
157 "#,
158 )
159 .execute(&self.pool)
160 .await?;
161
162 sqlx::query("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
164 .execute(&self.pool)
165 .await?;
166
167 if self.config.audit_table.is_some() {
169 sqlx::query(
170 r#"
171 CREATE TABLE IF NOT EXISTS auth_audit (
172 id INTEGER PRIMARY KEY AUTOINCREMENT,
173 username TEXT NOT NULL,
174 ip_address TEXT,
175 success INTEGER NOT NULL,
176 failure_reason TEXT,
177 timestamp TEXT DEFAULT CURRENT_TIMESTAMP
178 )
179 "#,
180 )
181 .execute(&self.pool)
182 .await?;
183
184 sqlx::query("CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON auth_audit(timestamp)")
186 .execute(&self.pool)
187 .await?;
188
189 sqlx::query("CREATE INDEX IF NOT EXISTS idx_audit_username ON auth_audit(username)")
191 .execute(&self.pool)
192 .await?;
193 }
194
195 Ok(())
196 }
197
198 #[allow(dead_code)]
200 async fn log_audit(
201 &self,
202 username: &str,
203 ip: Option<IpAddr>,
204 success: bool,
205 failure_reason: Option<&str>,
206 ) -> anyhow::Result<()> {
207 if self.config.audit_table.is_none() {
208 return Ok(());
209 }
210
211 let ip_str = ip.map(|i| i.to_string());
212
213 sqlx::query(
214 r#"
215 INSERT INTO auth_audit (username, ip_address, success, failure_reason)
216 VALUES (?, ?, ?, ?)
217 "#,
218 )
219 .bind(username)
220 .bind(ip_str)
221 .bind(if success { 1 } else { 0 })
222 .bind(failure_reason)
223 .execute(&self.pool)
224 .await?;
225
226 Ok(())
227 }
228
229 pub async fn get_user_metadata(
231 &self,
232 username: &Username,
233 ) -> anyhow::Result<Option<UserMetadata>> {
234 let row = sqlx::query(&self.config.password_query)
235 .bind(username.to_string())
236 .fetch_optional(&self.pool)
237 .await?;
238
239 let row = match row {
240 Some(r) => r,
241 None => return Ok(None),
242 };
243
244 let enabled: i64 = row.try_get("enabled")?;
245 let quota_bytes: i64 = row.try_get("quota_bytes")?;
246 let roles: Option<String> = row.try_get("roles").ok();
247
248 Ok(Some(UserMetadata {
249 enabled: enabled != 0,
250 quota_bytes,
251 roles,
252 }))
253 }
254
255 pub async fn get_audit_logs(
257 &self,
258 username: &str,
259 limit: i64,
260 ) -> anyhow::Result<Vec<AuditLog>> {
261 if self.config.audit_table.is_none() {
262 return Ok(Vec::new());
263 }
264
265 let rows = sqlx::query(
266 r#"
267 SELECT username, ip_address, success, failure_reason, timestamp
268 FROM auth_audit
269 WHERE username = ?
270 ORDER BY timestamp DESC
271 LIMIT ?
272 "#,
273 )
274 .bind(username)
275 .bind(limit)
276 .fetch_all(&self.pool)
277 .await?;
278
279 let mut logs = Vec::new();
280 for row in rows {
281 let log = AuditLog {
282 username: row.try_get("username")?,
283 ip_address: row.try_get("ip_address").ok(),
284 success: row.try_get::<i64, _>("success")? != 0,
285 failure_reason: row.try_get("failure_reason").ok(),
286 timestamp: row.try_get("timestamp")?,
287 };
288 logs.push(log);
289 }
290
291 Ok(logs)
292 }
293
294 fn verify_hash(&self, password: &str, hash: &str) -> anyhow::Result<bool> {
296 let hash_type = HashType::from_prefix(hash);
297
298 match hash_type {
299 HashType::Bcrypt => Ok(bcrypt::verify(password, hash)?),
300 HashType::Argon2 => {
301 use argon2::{Argon2, PasswordHash, PasswordVerifier};
302 let parsed_hash = PasswordHash::new(hash)
303 .map_err(|e| anyhow::anyhow!("Failed to parse Argon2 hash: {}", e))?;
304 Ok(Argon2::default()
305 .verify_password(password.as_bytes(), &parsed_hash)
306 .is_ok())
307 }
308 HashType::ScramSha256 => {
309 Err(anyhow::anyhow!(
311 "SCRAM-SHA-256 requires challenge/response authentication"
312 ))
313 }
314 }
315 }
316
317 fn hash_password(&self, password: &str) -> anyhow::Result<String> {
319 Ok(bcrypt::hash(password, bcrypt::DEFAULT_COST)?)
320 }
321}
322
323#[async_trait]
324impl AuthBackend for SqlBackend {
325 async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool> {
326 let row = sqlx::query(&self.config.password_query)
327 .bind(username.to_string())
328 .fetch_optional(&self.pool)
329 .await?;
330
331 let row = match row {
332 Some(r) => r,
333 None => {
334 let _ = self
335 .log_audit(&username.to_string(), None, false, Some("User not found"))
336 .await;
337 return Ok(false);
338 }
339 };
340
341 let password_hash: String = row.try_get("password_hash")?;
342 let enabled: i64 = row.try_get("enabled")?;
343
344 if enabled == 0 {
345 let _ = self
346 .log_audit(&username.to_string(), None, false, Some("User disabled"))
347 .await;
348 return Ok(false);
349 }
350
351 let verified = self.verify_hash(password, &password_hash)?;
352
353 if verified {
354 let _ = self
355 .log_audit(&username.to_string(), None, true, None)
356 .await;
357 } else {
358 let _ = self
359 .log_audit(&username.to_string(), None, false, Some("Invalid password"))
360 .await;
361 }
362
363 Ok(verified)
364 }
365
366 async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool> {
367 let row = sqlx::query(&self.config.password_query)
368 .bind(username.to_string())
369 .fetch_optional(&self.pool)
370 .await?;
371
372 Ok(row.is_some())
373 }
374
375 async fn list_users(&self) -> anyhow::Result<Vec<Username>> {
376 let rows = sqlx::query(&self.config.list_users_query)
377 .fetch_all(&self.pool)
378 .await?;
379
380 let users = rows
381 .into_iter()
382 .filter_map(|row| {
383 row.try_get::<String, _>("username")
384 .ok()
385 .and_then(|u| Username::new(u).ok())
386 })
387 .collect();
388
389 Ok(users)
390 }
391
392 async fn create_user(&self, username: &Username, password: &str) -> anyhow::Result<()> {
393 let password_hash = self.hash_password(password)?;
394
395 sqlx::query(&self.config.create_user_query)
396 .bind(username.to_string())
397 .bind(password_hash)
398 .bind("user") .execute(&self.pool)
400 .await?;
401
402 Ok(())
403 }
404
405 async fn delete_user(&self, username: &Username) -> anyhow::Result<()> {
406 sqlx::query(&self.config.delete_user_query)
407 .bind(username.to_string())
408 .execute(&self.pool)
409 .await?;
410
411 Ok(())
412 }
413
414 async fn change_password(&self, username: &Username, new_password: &str) -> anyhow::Result<()> {
415 let password_hash = self.hash_password(new_password)?;
416
417 sqlx::query(&self.config.update_password_query)
418 .bind(password_hash)
419 .bind(username.to_string())
420 .execute(&self.pool)
421 .await?;
422
423 Ok(())
424 }
425
426 async fn get_scram_params(&self, username: &str) -> anyhow::Result<(Vec<u8>, u32)> {
427 let query = self
428 .config
429 .scram_params_query
430 .as_ref()
431 .ok_or_else(|| anyhow::anyhow!("SCRAM parameters query not configured"))?;
432
433 let row = sqlx::query(query)
434 .bind(username)
435 .fetch_one(&self.pool)
436 .await?;
437
438 let salt: Vec<u8> = row.try_get("scram_salt")?;
439 let iterations: i64 = row.try_get("scram_iterations")?;
440
441 Ok((salt, iterations as u32))
442 }
443
444 async fn get_scram_stored_key(&self, username: &str) -> anyhow::Result<Vec<u8>> {
445 let query = self
446 .config
447 .scram_stored_key_query
448 .as_ref()
449 .ok_or_else(|| anyhow::anyhow!("SCRAM StoredKey query not configured"))?;
450
451 let row = sqlx::query(query)
452 .bind(username)
453 .fetch_one(&self.pool)
454 .await?;
455
456 Ok(row.try_get("scram_stored_key")?)
457 }
458
459 async fn get_scram_server_key(&self, username: &str) -> anyhow::Result<Vec<u8>> {
460 let query = self
461 .config
462 .scram_server_key_query
463 .as_ref()
464 .ok_or_else(|| anyhow::anyhow!("SCRAM ServerKey query not configured"))?;
465
466 let row = sqlx::query(query)
467 .bind(username)
468 .fetch_one(&self.pool)
469 .await?;
470
471 Ok(row.try_get("scram_server_key")?)
472 }
473
474 async fn store_scram_credentials(
475 &self,
476 username: &Username,
477 salt: Vec<u8>,
478 iterations: u32,
479 stored_key: Vec<u8>,
480 server_key: Vec<u8>,
481 ) -> anyhow::Result<()> {
482 let query = self
483 .config
484 .store_scram_query
485 .as_ref()
486 .ok_or_else(|| anyhow::anyhow!("SCRAM storage query not configured"))?;
487
488 sqlx::query(query)
489 .bind(&salt)
490 .bind(iterations as i64)
491 .bind(&stored_key)
492 .bind(&server_key)
493 .bind(username.to_string())
494 .execute(&self.pool)
495 .await?;
496
497 Ok(())
498 }
499}
500
501impl Clone for SqlBackend {
502 fn clone(&self) -> Self {
503 Self {
504 pool: self.pool.clone(),
505 config: self.config.clone(),
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use std::sync::atomic::{AtomicU64, Ordering};
514
515 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
516
517 fn unique_test_db_url() -> String {
525 let counter = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
526 let pid = std::process::id();
527 format!(
531 "sqlite:file:rusmes_auth_test_{}_{}?mode=memory&cache=shared",
532 pid, counter
533 )
534 }
535
536 async fn create_test_backend() -> SqlBackend {
537 let config = SqlConfig {
538 database_url: unique_test_db_url(),
539 ..Default::default()
540 };
541 let backend = SqlBackend::new(config)
542 .await
543 .expect("SqlBackend::new failed");
544 backend.init_schema().await.expect("init_schema failed");
545 backend
546 }
547
548 #[test]
549 fn test_hash_type_bcrypt() {
550 let hash = "$2b$12$KIXp8T/y7hOzQEu7qW3Ziu";
551 assert_eq!(HashType::from_prefix(hash), HashType::Bcrypt);
552 }
553
554 #[test]
555 fn test_hash_type_argon2() {
556 let hash = "$argon2id$v=19$m=65536,t=3,p=4$";
557 assert_eq!(HashType::from_prefix(hash), HashType::Argon2);
558 }
559
560 #[test]
561 fn test_hash_type_scram() {
562 let hash = "$scram-sha-256$iterations=4096";
563 assert_eq!(HashType::from_prefix(hash), HashType::ScramSha256);
564 }
565
566 #[test]
567 fn test_hash_type_default() {
568 let hash = "unknown_format";
569 assert_eq!(HashType::from_prefix(hash), HashType::Bcrypt);
570 }
571
572 #[test]
573 fn test_sql_config_default() {
574 let config = SqlConfig::default();
575 assert!(config.database_url.starts_with("sqlite:"));
576 assert_eq!(config.max_connections, 10);
577 assert!(config.scram_params_query.is_some());
578 }
579
580 #[test]
581 fn test_sql_config_custom() {
582 let config = SqlConfig {
583 database_url: "postgresql://localhost/rusmes".to_string(),
584 password_query: "SELECT hash FROM auth WHERE user = $1".to_string(),
585 max_connections: 20,
586 ..Default::default()
587 };
588 assert_eq!(config.database_url, "postgresql://localhost/rusmes");
589 assert_eq!(config.max_connections, 20);
590 }
591
592 #[tokio::test]
593 async fn test_sql_backend_creation() {
594 let _backend = create_test_backend().await;
595 }
596
597 #[tokio::test]
598 async fn test_init_schema() {
599 let _backend = create_test_backend().await;
600 }
601
602 #[tokio::test]
603 async fn test_create_and_verify_user() {
604 let backend = create_test_backend().await;
605
606 let username = Username::new("testuser".to_string()).unwrap();
607 let password = "testpass123";
608
609 backend.create_user(&username, password).await.unwrap();
610
611 let verified = backend.verify_identity(&username).await.unwrap();
612 assert!(verified);
613 }
614
615 #[tokio::test]
616 async fn test_authenticate_user() {
617 let backend = create_test_backend().await;
618
619 let username = Username::new("authuser".to_string()).unwrap();
620 let password = "secure_password";
621
622 backend.create_user(&username, password).await.unwrap();
623
624 let authenticated = backend.authenticate(&username, password).await.unwrap();
625 assert!(authenticated);
626
627 let wrong_auth = backend
628 .authenticate(&username, "wrong_password")
629 .await
630 .unwrap();
631 assert!(!wrong_auth);
632 }
633
634 #[tokio::test]
635 async fn test_list_users() {
636 let backend = create_test_backend().await;
637
638 backend
639 .create_user(&Username::new("user1".to_string()).unwrap(), "pass1")
640 .await
641 .unwrap();
642 backend
643 .create_user(&Username::new("user2".to_string()).unwrap(), "pass2")
644 .await
645 .unwrap();
646
647 let users = backend.list_users().await.unwrap();
648 assert_eq!(users.len(), 2);
649 }
650
651 #[tokio::test]
652 async fn test_delete_user() {
653 let backend = create_test_backend().await;
654
655 let username = Username::new("deleteuser".to_string()).unwrap();
656 backend.create_user(&username, "password").await.unwrap();
657
658 let exists_before = backend.verify_identity(&username).await.unwrap();
659 assert!(exists_before);
660
661 backend.delete_user(&username).await.unwrap();
662
663 let exists_after = backend.verify_identity(&username).await.unwrap();
664 assert!(!exists_after);
665 }
666
667 #[tokio::test]
668 async fn test_change_password() {
669 let backend = create_test_backend().await;
670
671 let username = Username::new("changepassuser".to_string()).unwrap();
672 let old_password = "oldpass";
673 let new_password = "newpass";
674
675 backend.create_user(&username, old_password).await.unwrap();
676
677 let auth_old = backend.authenticate(&username, old_password).await.unwrap();
678 assert!(auth_old);
679
680 backend
681 .change_password(&username, new_password)
682 .await
683 .unwrap();
684
685 let auth_new = backend.authenticate(&username, new_password).await.unwrap();
686 assert!(auth_new);
687
688 let auth_old_after = backend.authenticate(&username, old_password).await.unwrap();
689 assert!(!auth_old_after);
690 }
691
692 #[tokio::test]
693 async fn test_nonexistent_user() {
694 let backend = create_test_backend().await;
695
696 let username = Username::new("nonexistent".to_string()).unwrap();
697 let authenticated = backend
698 .authenticate(&username, "anypassword")
699 .await
700 .unwrap();
701 assert!(!authenticated);
702 }
703
704 #[test]
705 fn test_bcrypt_hash_verification() {
706 let password = "test_password";
707 let hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap();
708 let verified = bcrypt::verify(password, &hash).unwrap();
709 assert!(verified);
710 }
711
712 #[test]
713 fn test_password_query_format() {
714 let config = SqlConfig::default();
715 assert!(config.password_query.contains("SELECT"));
716 assert!(config.password_query.contains("password_hash"));
717 assert!(config.password_query.contains("enabled"));
718 }
719
720 #[test]
721 fn test_scram_queries_configured() {
722 let config = SqlConfig::default();
723 assert!(config.scram_params_query.is_some());
724 assert!(config.scram_stored_key_query.is_some());
725 assert!(config.scram_server_key_query.is_some());
726 assert!(config.store_scram_query.is_some());
727 }
728
729 #[tokio::test]
730 async fn test_multiple_users() {
731 let backend = create_test_backend().await;
732
733 for i in 0..5 {
734 let username = Username::new(format!("user{}", i)).unwrap();
735 backend
736 .create_user(&username, &format!("pass{}", i))
737 .await
738 .unwrap();
739 }
740
741 let users = backend.list_users().await.unwrap();
742 assert_eq!(users.len(), 5);
743 }
744
745 #[tokio::test]
746 async fn test_duplicate_username() {
747 let backend = create_test_backend().await;
748
749 let username = Username::new("duplicate".to_string()).unwrap();
750 backend.create_user(&username, "pass1").await.unwrap();
751 let result = backend.create_user(&username, "pass2").await;
752 assert!(result.is_err());
753 }
754
755 #[test]
756 fn test_hash_type_variants() {
757 assert_eq!(HashType::Bcrypt, HashType::Bcrypt);
758 assert_ne!(HashType::Bcrypt, HashType::Argon2);
759 assert_ne!(HashType::Argon2, HashType::ScramSha256);
760 }
761
762 #[tokio::test]
763 async fn test_empty_user_list() {
764 let backend = create_test_backend().await;
765
766 let users = backend.list_users().await.unwrap();
767 assert_eq!(users.len(), 0);
768 }
769
770 #[tokio::test]
771 async fn test_user_metadata() {
772 let backend = create_test_backend().await;
773
774 let username = Username::new("metauser".to_string()).unwrap();
775 backend.create_user(&username, "password").await.unwrap();
776
777 let metadata = backend.get_user_metadata(&username).await.unwrap();
778 assert!(metadata.is_some());
779
780 let meta = metadata.unwrap();
781 assert!(meta.enabled);
782 assert_eq!(meta.quota_bytes, 1073741824);
783 assert_eq!(meta.roles, Some("user".to_string()));
784 }
785
786 #[tokio::test]
787 async fn test_user_metadata_roles_vec() {
788 let metadata = UserMetadata {
789 enabled: true,
790 quota_bytes: 1000,
791 roles: Some("user,admin,moderator".to_string()),
792 };
793
794 let roles = metadata.roles_vec();
795 assert_eq!(roles.len(), 3);
796 assert!(roles.contains(&"user".to_string()));
797 assert!(roles.contains(&"admin".to_string()));
798 assert!(roles.contains(&"moderator".to_string()));
799 }
800
801 #[tokio::test]
802 async fn test_user_metadata_no_roles() {
803 let metadata = UserMetadata {
804 enabled: true,
805 quota_bytes: 1000,
806 roles: None,
807 };
808
809 let roles = metadata.roles_vec();
810 assert_eq!(roles.len(), 0);
811 }
812
813 #[tokio::test]
814 async fn test_audit_logging_success() {
815 let backend = create_test_backend().await;
816
817 let username = Username::new("audituser".to_string()).unwrap();
818 backend.create_user(&username, "password").await.unwrap();
819
820 backend.authenticate(&username, "password").await.unwrap();
821
822 let logs = backend.get_audit_logs("audituser", 10).await.unwrap();
823 assert!(!logs.is_empty());
824 }
825
826 #[tokio::test]
827 async fn test_audit_logging_failure() {
828 let backend = create_test_backend().await;
829
830 backend
831 .log_audit("nonexistent", None, false, Some("User not found"))
832 .await
833 .unwrap();
834
835 let logs = backend.get_audit_logs("nonexistent", 10).await.unwrap();
836 assert!(!logs.is_empty());
837 assert!(!logs[0].success);
838 }
839
840 #[tokio::test]
841 async fn test_audit_with_ip_address() {
842 let backend = create_test_backend().await;
843
844 let ip: IpAddr = "127.0.0.1".parse().unwrap();
845 backend
846 .log_audit("testuser", Some(ip), true, None)
847 .await
848 .unwrap();
849
850 let logs = backend.get_audit_logs("testuser", 10).await.unwrap();
851 assert!(!logs.is_empty());
852 assert_eq!(logs[0].ip_address, Some("127.0.0.1".to_string()));
853 }
854
855 #[tokio::test]
856 async fn test_audit_multiple_entries() {
857 let backend = create_test_backend().await;
858
859 for i in 0..5 {
860 backend
861 .log_audit(&format!("user{}", i), None, i % 2 == 0, None)
862 .await
863 .unwrap();
864 }
865
866 let logs = backend.get_audit_logs("user0", 10).await.unwrap();
867 assert!(!logs.is_empty());
868 }
869
870 #[tokio::test]
871 async fn test_audit_limit() {
872 let backend = create_test_backend().await;
873
874 for _ in 0..10 {
875 backend
876 .log_audit("limituser", None, true, None)
877 .await
878 .unwrap();
879 }
880
881 let logs = backend.get_audit_logs("limituser", 5).await.unwrap();
882 assert_eq!(logs.len(), 5);
883 }
884
885 #[tokio::test]
886 #[ignore = "stress: SQLite connection pool under concurrent bcrypt load; run manually with --ignored"]
887 async fn test_connection_pool() {
888 let config = SqlConfig {
907 database_url: unique_test_db_url(),
908 max_connections: 20,
909 ..Default::default()
910 };
911 let backend = SqlBackend::new(config)
912 .await
913 .expect("SqlBackend::new failed");
914 backend.init_schema().await.expect("init_schema failed");
915
916 let mut handles = vec![];
919 for i in 0..10 {
920 let username = Username::new(format!("pooluser{}", i)).expect("Username::new failed");
921 let password = format!("pass{}", i);
922 let b = backend.clone();
923 let handle = tokio::spawn(async move { b.create_user(&username, &password).await });
924 handles.push(handle);
925 }
926
927 for handle in handles {
928 let result = handle.await.expect("task panicked");
929 assert!(result.is_ok(), "create_user failed: {:?}", result.err());
930 }
931 }
932
933 #[tokio::test]
934 async fn test_argon2_hash_verification() {
935 use argon2::password_hash::{rand_core::OsRng, PasswordHash, SaltString};
936 use argon2::{Argon2, PasswordHasher, PasswordVerifier};
937
938 let password = "test_password";
939 let salt = SaltString::generate(&mut OsRng);
940 let argon2 = Argon2::default();
941 let hash = argon2
942 .hash_password(password.as_bytes(), &salt)
943 .unwrap()
944 .to_string();
945
946 let parsed = PasswordHash::new(&hash).unwrap();
947 let verify_result = Argon2::default().verify_password(password.as_bytes(), &parsed);
948 assert!(verify_result.is_ok());
949 }
950
951 #[tokio::test]
952 async fn test_scram_hash_error() {
953 let config = SqlConfig {
954 database_url: unique_test_db_url(),
955 ..Default::default()
956 };
957 let backend = SqlBackend::new(config)
958 .await
959 .expect("SqlBackend::new failed");
960
961 let result = backend.verify_hash("password", "$scram-sha-256$test");
962 assert!(result.is_err());
963 }
964
965 #[tokio::test]
966 async fn test_verify_nonexistent_user() {
967 let backend = create_test_backend().await;
968
969 let username = Username::new("ghost".to_string()).unwrap();
970 let verified = backend.verify_identity(&username).await.unwrap();
971 assert!(!verified);
972 }
973
974 #[tokio::test]
975 async fn test_password_hash_different() {
976 let config = SqlConfig {
977 database_url: unique_test_db_url(),
978 ..Default::default()
979 };
980 let backend = SqlBackend::new(config)
981 .await
982 .expect("SqlBackend::new failed");
983
984 let hash1 = backend
985 .hash_password("password")
986 .expect("hash_password failed");
987 let hash2 = backend
988 .hash_password("password")
989 .expect("hash_password failed");
990
991 assert_ne!(hash1, hash2);
993
994 assert!(backend.verify_hash("password", &hash1).unwrap());
996 assert!(backend.verify_hash("password", &hash2).unwrap());
997 }
998
999 #[tokio::test]
1000 async fn test_special_characters_in_username() {
1001 let backend = create_test_backend().await;
1002
1003 let username = Username::new("test.user+tag@example.com".to_string()).unwrap();
1004 backend.create_user(&username, "password").await.unwrap();
1005
1006 let authenticated = backend.authenticate(&username, "password").await.unwrap();
1007 assert!(authenticated);
1008 }
1009
1010 #[tokio::test]
1011 async fn test_long_password() {
1012 let backend = create_test_backend().await;
1013
1014 let username = Username::new("longpassuser".to_string()).unwrap();
1015 let password = "a".repeat(100);
1016
1017 backend.create_user(&username, &password).await.unwrap();
1018
1019 let authenticated = backend.authenticate(&username, &password).await.unwrap();
1020 assert!(authenticated);
1021 }
1022
1023 #[tokio::test]
1024 async fn test_empty_password_rejection() {
1025 let backend = create_test_backend().await;
1026
1027 let username = Username::new("emptypass".to_string()).unwrap();
1028 backend.create_user(&username, "").await.unwrap();
1029
1030 let authenticated = backend.authenticate(&username, "").await.unwrap();
1031 assert!(authenticated);
1032
1033 let auth_wrong = backend.authenticate(&username, "notblank").await.unwrap();
1034 assert!(!auth_wrong);
1035 }
1036
1037 #[tokio::test]
1038 async fn test_case_sensitive_password() {
1039 let backend = create_test_backend().await;
1040
1041 let username = Username::new("caseuser".to_string()).unwrap();
1042 backend.create_user(&username, "Password123").await.unwrap();
1043
1044 let auth_correct = backend
1045 .authenticate(&username, "Password123")
1046 .await
1047 .unwrap();
1048 assert!(auth_correct);
1049
1050 let auth_wrong = backend
1051 .authenticate(&username, "password123")
1052 .await
1053 .unwrap();
1054 assert!(!auth_wrong);
1055 }
1056
1057 #[tokio::test]
1058 async fn test_concurrent_authentication() {
1059 let backend = create_test_backend().await;
1060
1061 let username = Username::new("concurrent".to_string()).unwrap();
1062 backend.create_user(&username, "password").await.unwrap();
1063
1064 let mut handles = vec![];
1065 for _ in 0..10 {
1066 let b = backend.clone();
1067 let u = username.clone();
1068 let handle = tokio::spawn(async move { b.authenticate(&u, "password").await });
1069 handles.push(handle);
1070 }
1071
1072 for handle in handles {
1073 assert!(handle.await.unwrap().unwrap());
1074 }
1075 }
1076
1077 #[tokio::test]
1078 async fn test_database_connection_reuse() {
1079 let backend = create_test_backend().await;
1080
1081 for i in 0..20 {
1082 let username = Username::new(format!("reuse{}", i)).unwrap();
1083 backend.create_user(&username, "password").await.unwrap();
1084 }
1085
1086 let users = backend.list_users().await.unwrap();
1087 assert_eq!(users.len(), 20);
1088 }
1089
1090 #[test]
1091 fn test_hash_type_copy_trait() {
1092 let hash_type = HashType::Bcrypt;
1093 let copied = hash_type;
1094 assert_eq!(hash_type, copied);
1095 }
1096
1097 #[test]
1098 fn test_user_metadata_clone() {
1099 let metadata = UserMetadata {
1100 enabled: true,
1101 quota_bytes: 1000,
1102 roles: Some("admin".to_string()),
1103 };
1104 let cloned = metadata.clone();
1105 assert_eq!(cloned.enabled, metadata.enabled);
1106 assert_eq!(cloned.quota_bytes, metadata.quota_bytes);
1107 }
1108
1109 #[test]
1110 fn test_audit_log_debug() {
1111 let log = AuditLog {
1112 username: "test".to_string(),
1113 ip_address: Some("127.0.0.1".to_string()),
1114 success: true,
1115 failure_reason: None,
1116 timestamp: "2025-01-01 00:00:00".to_string(),
1117 };
1118 let debug_str = format!("{:?}", log);
1119 assert!(debug_str.contains("test"));
1120 }
1121
1122 #[tokio::test]
1123 async fn test_scram_params_not_configured() {
1124 let config = SqlConfig {
1125 database_url: unique_test_db_url(),
1126 scram_params_query: None,
1127 ..Default::default()
1128 };
1129 let backend = SqlBackend::new(config)
1130 .await
1131 .expect("SqlBackend::new failed");
1132 backend.init_schema().await.expect("init_schema failed");
1133
1134 let result = backend.get_scram_params("testuser").await;
1135 assert!(result.is_err());
1136 }
1137
1138 #[tokio::test]
1139 async fn test_scram_stored_key_not_configured() {
1140 let config = SqlConfig {
1141 database_url: unique_test_db_url(),
1142 scram_stored_key_query: None,
1143 ..Default::default()
1144 };
1145 let backend = SqlBackend::new(config)
1146 .await
1147 .expect("SqlBackend::new failed");
1148 backend.init_schema().await.expect("init_schema failed");
1149
1150 let result = backend.get_scram_stored_key("testuser").await;
1151 assert!(result.is_err());
1152 }
1153
1154 #[tokio::test]
1155 async fn test_scram_server_key_not_configured() {
1156 let config = SqlConfig {
1157 database_url: unique_test_db_url(),
1158 scram_server_key_query: None,
1159 ..Default::default()
1160 };
1161 let backend = SqlBackend::new(config)
1162 .await
1163 .expect("SqlBackend::new failed");
1164 backend.init_schema().await.expect("init_schema failed");
1165
1166 let result = backend.get_scram_server_key("testuser").await;
1167 assert!(result.is_err());
1168 }
1169
1170 #[tokio::test]
1171 async fn test_store_scram_not_configured() {
1172 let config = SqlConfig {
1173 database_url: unique_test_db_url(),
1174 store_scram_query: None,
1175 ..Default::default()
1176 };
1177 let backend = SqlBackend::new(config)
1178 .await
1179 .expect("SqlBackend::new failed");
1180 backend.init_schema().await.expect("init_schema failed");
1181
1182 let username = Username::new("scram".to_string()).expect("Username::new failed");
1183 let result = backend
1184 .store_scram_credentials(&username, vec![1, 2, 3], 4096, vec![4, 5, 6], vec![7, 8, 9])
1185 .await;
1186 assert!(result.is_err());
1187 }
1188
1189 #[tokio::test]
1190 async fn test_audit_disabled() {
1191 let config = SqlConfig {
1192 database_url: unique_test_db_url(),
1193 audit_table: None,
1194 ..Default::default()
1195 };
1196 let backend = SqlBackend::new(config)
1197 .await
1198 .expect("SqlBackend::new failed");
1199 backend.init_schema().await.expect("init_schema failed");
1200
1201 backend.log_audit("user", None, true, None).await.unwrap();
1203
1204 let logs = backend.get_audit_logs("user", 10).await.unwrap();
1205 assert_eq!(logs.len(), 0);
1206 }
1207
1208 #[tokio::test]
1209 async fn test_user_metadata_nonexistent() {
1210 let backend = create_test_backend().await;
1211
1212 let username = Username::new("phantom".to_string()).unwrap();
1213 let metadata = backend.get_user_metadata(&username).await.unwrap();
1214 assert!(metadata.is_none());
1215 }
1216
1217 #[tokio::test]
1218 async fn test_custom_database_url() {
1219 let config = SqlConfig {
1222 database_url: unique_test_db_url(),
1223 ..Default::default()
1224 };
1225 let backend = SqlBackend::new(config).await;
1226 assert!(backend.is_ok());
1227 }
1228}