1use crate::AuthBackend;
4use async_trait::async_trait;
5use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
6use rusmes_proto::Username;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11#[derive(Debug, Clone)]
13pub struct LdapConfig {
14 pub server_url: String,
16 pub base_dn: String,
18 pub user_filter: String,
20 pub bind_dn_pattern: Option<String>,
23 pub bind_dn: Option<String>,
25 pub bind_password: Option<String>,
27 pub group_base_dn: Option<String>,
29 pub group_filter: Option<String>,
31 pub required_group: Option<String>,
33 pub timeout_secs: u64,
35 pub pool_size: usize,
37 pub use_tls: bool,
39 pub tls_skip_verify: bool,
41}
42
43impl Default for LdapConfig {
44 fn default() -> Self {
45 Self {
46 server_url: "ldap://localhost:389".to_string(),
47 base_dn: "dc=example,dc=com".to_string(),
48 user_filter: "(uid={username})".to_string(),
49 bind_dn_pattern: None,
50 bind_dn: None,
51 bind_password: None,
52 group_base_dn: None,
53 group_filter: None,
54 required_group: None,
55 timeout_secs: 10,
56 pool_size: 5,
57 use_tls: false,
58 tls_skip_verify: false,
59 }
60 }
61}
62
63struct ConnectionPool {
65 config: LdapConfig,
66 connections: Arc<RwLock<Vec<Ldap>>>,
67 max_size: usize,
68}
69
70impl ConnectionPool {
71 fn new(config: LdapConfig) -> Self {
72 let max_size = config.pool_size;
73 Self {
74 config,
75 connections: Arc::new(RwLock::new(Vec::new())),
76 max_size,
77 }
78 }
79
80 async fn get_connection(&self) -> anyhow::Result<Ldap> {
81 {
83 let mut pool = self.connections.write().await;
84 if let Some(conn) = pool.pop() {
85 return Ok(conn);
86 }
87 }
88
89 self.create_connection().await
91 }
92
93 async fn create_connection(&self) -> anyhow::Result<Ldap> {
94 let settings = LdapConnSettings::new()
95 .set_conn_timeout(std::time::Duration::from_secs(self.config.timeout_secs));
96
97 let (conn, mut ldap) =
98 LdapConnAsync::with_settings(settings, &self.config.server_url).await?;
99
100 ldap3::drive!(conn);
101
102 if let (Some(bind_dn), Some(bind_password)) =
104 (&self.config.bind_dn, &self.config.bind_password)
105 {
106 ldap.simple_bind(bind_dn, bind_password).await?;
107 }
108
109 Ok(ldap)
110 }
111
112 async fn return_connection(&self, conn: Ldap) {
113 let mut pool = self.connections.write().await;
114 if pool.len() < self.max_size {
115 pool.push(conn);
116 }
117 }
118}
119
120pub struct LdapBackend {
122 config: LdapConfig,
123 pool: ConnectionPool,
124 user_cache: Arc<RwLock<HashMap<String, bool>>>,
125}
126
127impl LdapBackend {
128 pub fn new(config: LdapConfig) -> Self {
130 let pool = ConnectionPool::new(config.clone());
131 Self {
132 config,
133 pool,
134 user_cache: Arc::new(RwLock::new(HashMap::new())),
135 }
136 }
137
138 async fn get_user_dn(&self, username: &str) -> anyhow::Result<Option<String>> {
140 if let Some(pattern) = &self.config.bind_dn_pattern {
142 let dn = pattern.replace("{username}", username);
143 return Ok(Some(dn));
144 }
145
146 self.search_user(username).await
148 }
149
150 async fn search_user(&self, username: &str) -> anyhow::Result<Option<String>> {
152 let mut ldap = self.pool.get_connection().await?;
153
154 let filter = self.config.user_filter.replace("{username}", username);
155
156 let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
157 let result = tokio::time::timeout(
158 timeout,
159 ldap.search(&self.config.base_dn, Scope::Subtree, &filter, vec!["dn"]),
160 )
161 .await
162 .map_err(|_| anyhow::anyhow!("LDAP search timeout"))??;
163
164 let (entries, _res) = result.success()?;
165
166 let dn = if entries.is_empty() {
167 None
168 } else {
169 let entry = SearchEntry::construct(entries[0].clone());
170 Some(entry.dn)
171 };
172
173 self.pool.return_connection(ldap).await;
174
175 Ok(dn)
176 }
177
178 async fn bind_as_user(&self, dn: &str, password: &str) -> anyhow::Result<bool> {
180 let mut ldap = self.pool.create_connection().await?;
181
182 let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
183 match tokio::time::timeout(timeout, ldap.simple_bind(dn, password)).await {
184 Ok(Ok(_)) => {
185 let _ = ldap.unbind().await;
186 Ok(true)
187 }
188 Ok(Err(_)) => Ok(false),
189 Err(_) => Err(anyhow::anyhow!("LDAP bind timeout")),
190 }
191 }
192
193 async fn check_group_membership(&self, username: &str) -> anyhow::Result<bool> {
195 if self.config.required_group.is_none() {
196 return Ok(true);
197 }
198
199 let group_base = match &self.config.group_base_dn {
200 Some(base) => base,
201 None => &self.config.base_dn,
202 };
203
204 let filter = match &self.config.group_filter {
205 Some(f) => f.replace("{username}", username),
206 None => format!("(memberUid={})", username),
207 };
208
209 let mut ldap = self.pool.get_connection().await?;
210
211 let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
212 let result = tokio::time::timeout(
213 timeout,
214 ldap.search(group_base, Scope::Subtree, &filter, vec!["dn"]),
215 )
216 .await
217 .map_err(|_| anyhow::anyhow!("LDAP group search timeout"))??;
218
219 let (entries, _res) = result.success()?;
220
221 self.pool.return_connection(ldap).await;
222
223 if let Some(required_group) = &self.config.required_group {
224 Ok(entries.iter().any(|entry| {
225 let e = SearchEntry::construct(entry.clone());
226 &e.dn == required_group
227 }))
228 } else {
229 Ok(!entries.is_empty())
230 }
231 }
232}
233
234#[async_trait]
235impl AuthBackend for LdapBackend {
236 async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool> {
237 let dn = match self.get_user_dn(&username.to_string()).await? {
239 Some(dn) => dn,
240 None => return Ok(false),
241 };
242
243 let bind_success = self.bind_as_user(&dn, password).await?;
245 if !bind_success {
246 return Ok(false);
247 }
248
249 if !self.check_group_membership(&username.to_string()).await? {
251 return Ok(false);
252 }
253
254 self.user_cache
256 .write()
257 .await
258 .insert(username.to_string(), true);
259
260 Ok(true)
261 }
262
263 async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool> {
264 {
266 let cache = self.user_cache.read().await;
267 if cache.contains_key(&username.to_string()) {
268 return Ok(true);
269 }
270 }
271
272 let exists = self.search_user(&username.to_string()).await?.is_some();
274
275 if exists {
276 self.user_cache
277 .write()
278 .await
279 .insert(username.to_string(), true);
280 }
281
282 Ok(exists)
283 }
284
285 async fn list_users(&self) -> anyhow::Result<Vec<Username>> {
286 let mut ldap = self.pool.get_connection().await?;
287
288 let filter = self.config.user_filter.replace("{username}", "*");
289 let result = ldap
290 .search(
291 &self.config.base_dn,
292 Scope::Subtree,
293 &filter,
294 vec!["uid", "mail"],
295 )
296 .await?;
297
298 let (entries, _res) = result.success()?;
299
300 self.pool.return_connection(ldap).await;
301
302 let users = entries
303 .into_iter()
304 .filter_map(|entry| {
305 let e = SearchEntry::construct(entry);
306 e.attrs
307 .get("uid")
308 .and_then(|uids| uids.first().and_then(|uid| Username::new(uid.clone()).ok()))
309 })
310 .collect();
311
312 Ok(users)
313 }
314
315 async fn create_user(&self, _username: &Username, _password: &str) -> anyhow::Result<()> {
316 Err(anyhow::anyhow!(
317 "LDAP backend does not support user creation (read-only)"
318 ))
319 }
320
321 async fn delete_user(&self, _username: &Username) -> anyhow::Result<()> {
322 Err(anyhow::anyhow!(
323 "LDAP backend does not support user deletion (read-only)"
324 ))
325 }
326
327 async fn change_password(
328 &self,
329 _username: &Username,
330 _new_password: &str,
331 ) -> anyhow::Result<()> {
332 Err(anyhow::anyhow!(
333 "LDAP backend does not support password changes (read-only)"
334 ))
335 }
336}
337
338impl Clone for LdapBackend {
339 fn clone(&self) -> Self {
340 Self {
341 config: self.config.clone(),
342 pool: ConnectionPool::new(self.config.clone()),
343 user_cache: Arc::clone(&self.user_cache),
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_ldap_config_default() {
354 let config = LdapConfig::default();
355 assert_eq!(config.server_url, "ldap://localhost:389");
356 assert_eq!(config.base_dn, "dc=example,dc=com");
357 assert_eq!(config.user_filter, "(uid={username})");
358 assert_eq!(config.timeout_secs, 10);
359 assert_eq!(config.pool_size, 5);
360 }
361
362 #[test]
363 fn test_ldap_config_custom() {
364 let config = LdapConfig {
365 server_url: "ldaps://ldap.example.com:636".to_string(),
366 base_dn: "ou=users,dc=example,dc=org".to_string(),
367 user_filter: "(mail={username}@example.com)".to_string(),
368 bind_dn_pattern: None,
369 bind_dn: Some("cn=admin,dc=example,dc=org".to_string()),
370 bind_password: Some("secret".to_string()),
371 group_base_dn: Some("ou=groups,dc=example,dc=org".to_string()),
372 group_filter: Some(
373 "(&(objectClass=groupOfNames)(member=uid={username},ou=users,dc=example,dc=org))"
374 .to_string(),
375 ),
376 required_group: Some("cn=mail-users,ou=groups,dc=example,dc=org".to_string()),
377 timeout_secs: 30,
378 pool_size: 10,
379 use_tls: true,
380 tls_skip_verify: false,
381 };
382 assert_eq!(config.timeout_secs, 30);
383 assert_eq!(config.pool_size, 10);
384 }
385
386 #[test]
387 fn test_connection_pool_creation() {
388 let config = LdapConfig::default();
389 let pool = ConnectionPool::new(config.clone());
390 assert_eq!(pool.max_size, config.pool_size);
391 }
392
393 #[tokio::test]
394 async fn test_ldap_backend_creation() {
395 let config = LdapConfig::default();
396 let backend = LdapBackend::new(config);
397 let cache = backend.user_cache.read().await;
398 assert_eq!(cache.len(), 0);
399 }
400
401 #[tokio::test]
402 async fn test_user_filter_substitution() {
403 let config = LdapConfig {
404 user_filter: "(uid={username})".to_string(),
405 ..Default::default()
406 };
407 let filter = config.user_filter.replace("{username}", "testuser");
408 assert_eq!(filter, "(uid=testuser)");
409 }
410
411 #[tokio::test]
412 async fn test_group_filter_substitution() {
413 let config = LdapConfig {
414 group_filter: Some("(memberUid={username})".to_string()),
415 ..Default::default()
416 };
417 let filter = config
418 .group_filter
419 .unwrap()
420 .replace("{username}", "testuser");
421 assert_eq!(filter, "(memberUid=testuser)");
422 }
423
424 #[tokio::test]
425 async fn test_verify_identity_cache() {
426 let backend = LdapBackend::new(LdapConfig::default());
427 let _username = Username::new("cached_user".to_string()).unwrap();
428
429 backend
431 .user_cache
432 .write()
433 .await
434 .insert("cached_user".to_string(), true);
435
436 let cache = backend.user_cache.read().await;
438 assert!(cache.contains_key("cached_user"));
439 }
440
441 #[tokio::test]
442 async fn test_create_user_not_supported() {
443 let backend = LdapBackend::new(LdapConfig::default());
444 let username = Username::new("newuser".to_string()).unwrap();
445 let result = backend.create_user(&username, "password").await;
446 assert!(result.is_err());
447 assert!(result.unwrap_err().to_string().contains("read-only"));
448 }
449
450 #[tokio::test]
451 async fn test_delete_user_not_supported() {
452 let backend = LdapBackend::new(LdapConfig::default());
453 let username = Username::new("user".to_string()).unwrap();
454 let result = backend.delete_user(&username).await;
455 assert!(result.is_err());
456 assert!(result.unwrap_err().to_string().contains("read-only"));
457 }
458
459 #[tokio::test]
460 async fn test_change_password_not_supported() {
461 let backend = LdapBackend::new(LdapConfig::default());
462 let username = Username::new("user".to_string()).unwrap();
463 let result = backend.change_password(&username, "newpass").await;
464 assert!(result.is_err());
465 assert!(result.unwrap_err().to_string().contains("read-only"));
466 }
467
468 #[test]
469 fn test_ldap_config_with_bind_credentials() {
470 let config = LdapConfig {
471 bind_dn: Some("cn=admin,dc=example,dc=com".to_string()),
472 bind_password: Some("admin_password".to_string()),
473 ..Default::default()
474 };
475 assert!(config.bind_dn.is_some());
476 assert!(config.bind_password.is_some());
477 }
478
479 #[test]
480 fn test_ldap_config_without_bind_credentials() {
481 let config = LdapConfig::default();
482 assert!(config.bind_dn.is_none());
483 assert!(config.bind_password.is_none());
484 }
485
486 #[test]
487 fn test_required_group_configuration() {
488 let config = LdapConfig {
489 required_group: Some("cn=email-users,ou=groups,dc=example,dc=com".to_string()),
490 ..Default::default()
491 };
492 assert!(config.required_group.is_some());
493 }
494
495 #[test]
496 fn test_group_base_dn_configuration() {
497 let config = LdapConfig {
498 group_base_dn: Some("ou=groups,dc=example,dc=com".to_string()),
499 ..Default::default()
500 };
501 assert!(config.group_base_dn.is_some());
502 }
503
504 #[tokio::test]
505 async fn test_connection_pool_size() {
506 let config = LdapConfig {
507 pool_size: 3,
508 ..Default::default()
509 };
510 let pool = ConnectionPool::new(config);
511 assert_eq!(pool.max_size, 3);
512 }
513
514 #[test]
515 fn test_complex_user_filter() {
516 let config = LdapConfig {
517 user_filter: "(&(objectClass=person)(|(uid={username})(mail={username})))".to_string(),
518 ..Default::default()
519 };
520 let filter = config.user_filter.replace("{username}", "john");
521 assert!(filter.contains("uid=john"));
522 assert!(filter.contains("mail=john"));
523 }
524
525 #[test]
526 fn test_complex_group_filter() {
527 let config = LdapConfig {
528 group_filter: Some(
529 "(&(objectClass=groupOfNames)(member=uid={username},ou=users,dc=example,dc=com))"
530 .to_string(),
531 ),
532 ..Default::default()
533 };
534 let filter = config.group_filter.unwrap().replace("{username}", "jane");
535 assert!(filter.contains("uid=jane"));
536 }
537
538 #[test]
539 fn test_timeout_configuration() {
540 let config = LdapConfig {
541 timeout_secs: 60,
542 ..Default::default()
543 };
544 assert_eq!(config.timeout_secs, 60);
545 }
546
547 #[test]
548 fn test_ldaps_url() {
549 let config = LdapConfig {
550 server_url: "ldaps://secure-ldap.example.com:636".to_string(),
551 ..Default::default()
552 };
553 assert!(config.server_url.starts_with("ldaps://"));
554 }
555
556 #[tokio::test]
557 async fn test_cache_empty_on_creation() {
558 let backend = LdapBackend::new(LdapConfig::default());
559 let cache = backend.user_cache.read().await;
560 assert!(cache.is_empty());
561 }
562
563 #[tokio::test]
564 async fn test_cache_insertion() {
565 let backend = LdapBackend::new(LdapConfig::default());
566 backend
567 .user_cache
568 .write()
569 .await
570 .insert("user1".to_string(), true);
571 backend
572 .user_cache
573 .write()
574 .await
575 .insert("user2".to_string(), true);
576
577 let cache = backend.user_cache.read().await;
578 assert_eq!(cache.len(), 2);
579 assert!(cache.contains_key("user1"));
580 assert!(cache.contains_key("user2"));
581 }
582
583 #[test]
584 fn test_bind_dn_pattern() {
585 let config = LdapConfig {
586 bind_dn_pattern: Some("uid={username},ou=users,dc=example,dc=com".to_string()),
587 ..Default::default()
588 };
589 assert!(config.bind_dn_pattern.is_some());
590 let dn = config
591 .bind_dn_pattern
592 .unwrap()
593 .replace("{username}", "alice");
594 assert_eq!(dn, "uid=alice,ou=users,dc=example,dc=com");
595 }
596
597 #[test]
598 fn test_bind_dn_pattern_with_email() {
599 let config = LdapConfig {
600 bind_dn_pattern: Some(
601 "mail={username}@example.com,ou=users,dc=example,dc=com".to_string(),
602 ),
603 ..Default::default()
604 };
605 let dn = config.bind_dn_pattern.unwrap().replace("{username}", "bob");
606 assert_eq!(dn, "mail=bob@example.com,ou=users,dc=example,dc=com");
607 }
608
609 #[test]
610 fn test_tls_configuration() {
611 let config = LdapConfig {
612 use_tls: true,
613 server_url: "ldaps://ldap.example.com:636".to_string(),
614 ..Default::default()
615 };
616 assert!(config.use_tls);
617 assert!(config.server_url.starts_with("ldaps://"));
618 }
619
620 #[test]
621 fn test_tls_skip_verify_configuration() {
622 let config = LdapConfig {
623 use_tls: true,
624 tls_skip_verify: true,
625 ..Default::default()
626 };
627 assert!(config.use_tls);
628 assert!(config.tls_skip_verify);
629 }
630
631 #[test]
632 fn test_multiple_user_filter_patterns() {
633 let config = LdapConfig {
634 user_filter:
635 "(&(objectClass=inetOrgPerson)(|(uid={username})(mail={username}@example.com)))"
636 .to_string(),
637 ..Default::default()
638 };
639 let filter = config.user_filter.replace("{username}", "charlie");
640 assert!(filter.contains("uid=charlie"));
641 assert!(filter.contains("mail=charlie@example.com"));
642 }
643
644 #[test]
645 fn test_memberof_group_filter() {
646 let config = LdapConfig {
647 group_filter: Some("(memberOf=cn=mail-users,ou=groups,dc=example,dc=com)".to_string()),
648 ..Default::default()
649 };
650 assert!(config.group_filter.is_some());
651 }
652
653 #[tokio::test]
654 async fn test_connection_pool_multiple_returns() {
655 let config = LdapConfig {
656 pool_size: 2,
657 ..Default::default()
658 };
659 let pool = ConnectionPool::new(config);
660
661 let connections = pool.connections.read().await;
663 assert_eq!(connections.len(), 0);
664 }
665
666 #[test]
667 fn test_ldap_config_clone() {
668 let config1 = LdapConfig {
669 server_url: "ldap://test.com:389".to_string(),
670 timeout_secs: 20,
671 ..Default::default()
672 };
673 let config2 = config1.clone();
674 assert_eq!(config1.server_url, config2.server_url);
675 assert_eq!(config1.timeout_secs, config2.timeout_secs);
676 }
677
678 #[test]
679 fn test_empty_group_base_dn_fallback() {
680 let config = LdapConfig {
681 base_dn: "dc=example,dc=org".to_string(),
682 group_base_dn: None,
683 ..Default::default()
684 };
685 let group_base = config.group_base_dn.as_ref().unwrap_or(&config.base_dn);
686 assert_eq!(group_base, "dc=example,dc=org");
687 }
688
689 #[test]
690 fn test_custom_timeout() {
691 let config = LdapConfig {
692 timeout_secs: 5,
693 ..Default::default()
694 };
695 assert_eq!(config.timeout_secs, 5);
696 }
697
698 #[test]
699 fn test_large_pool_size() {
700 let config = LdapConfig {
701 pool_size: 100,
702 ..Default::default()
703 };
704 assert_eq!(config.pool_size, 100);
705 }
706
707 #[tokio::test]
708 async fn test_cache_concurrent_access() {
709 let backend = LdapBackend::new(LdapConfig::default());
710
711 let backend1 = backend.clone();
713 let backend2 = backend.clone();
714
715 let handle1 = tokio::spawn(async move {
716 backend1
717 .user_cache
718 .write()
719 .await
720 .insert("user_a".to_string(), true);
721 });
722
723 let handle2 = tokio::spawn(async move {
724 backend2
725 .user_cache
726 .write()
727 .await
728 .insert("user_b".to_string(), true);
729 });
730
731 let _ = handle1.await;
732 let _ = handle2.await;
733
734 let cache = backend.user_cache.read().await;
735 assert!(cache.len() <= 2);
736 }
737
738 #[test]
739 fn test_ldap_url_with_port() {
740 let config = LdapConfig {
741 server_url: "ldap://ldap.company.com:389".to_string(),
742 ..Default::default()
743 };
744 assert!(config.server_url.contains(":389"));
745 }
746
747 #[test]
748 fn test_ldaps_url_with_port() {
749 let config = LdapConfig {
750 server_url: "ldaps://ldap.company.com:636".to_string(),
751 ..Default::default()
752 };
753 assert!(config.server_url.contains(":636"));
754 }
755
756 #[test]
757 fn test_active_directory_user_filter() {
758 let config = LdapConfig {
759 user_filter: "(&(objectClass=user)(sAMAccountName={username}))".to_string(),
760 ..Default::default()
761 };
762 let filter = config.user_filter.replace("{username}", "jdoe");
763 assert!(filter.contains("sAMAccountName=jdoe"));
764 }
765
766 #[test]
767 fn test_active_directory_group_filter() {
768 let config = LdapConfig {
769 group_filter: Some("(&(objectClass=group)(member={dn}))".to_string()),
770 ..Default::default()
771 };
772 assert!(config.group_filter.is_some());
773 }
774
775 #[test]
776 fn test_posix_group_filter() {
777 let config = LdapConfig {
778 group_filter: Some("(&(objectClass=posixGroup)(memberUid={username}))".to_string()),
779 ..Default::default()
780 };
781 let filter = config
782 .group_filter
783 .unwrap()
784 .replace("{username}", "user123");
785 assert!(filter.contains("memberUid=user123"));
786 }
787
788 #[tokio::test]
789 async fn test_ldap_backend_clone() {
790 let backend1 = LdapBackend::new(LdapConfig::default());
791 let backend2 = backend1.clone();
792
793 backend1
795 .user_cache
796 .write()
797 .await
798 .insert("test".to_string(), true);
799 let cache2 = backend2.user_cache.read().await;
800 assert!(cache2.contains_key("test"));
801 }
802}