1use std::{borrow::Cow, collections::HashMap};
2
3use parse_display::{Display, FromStr};
4use testcontainers::{
5 core::{ContainerPort, WaitFor},
6 CopyDataSource, CopyToContainer, Image,
7};
8
9const NAME: &str = "bitnami/openldap";
10const TAG: &str = "2.6.8";
11const OPENLDAP_PORT: ContainerPort = ContainerPort::Tcp(1389);
12const OPENLDAPS_PORT: ContainerPort = ContainerPort::Tcp(1636);
13
14#[derive(Debug, Clone)]
47pub struct OpenLDAP {
48 env_vars: HashMap<String, String>,
49 users: Vec<User>,
50 copy_to_sources: Vec<CopyToContainer>,
51}
52#[derive(Debug, Clone)]
53struct User {
54 username: String,
55 password: String,
56}
57
58impl OpenLDAP {
59 pub fn with_base_dn(mut self, base_dn: impl ToString) -> Self {
62 self.env_vars
63 .insert("LDAP_ROOT".to_owned(), base_dn.to_string());
64 self
65 }
66 pub fn with_admin(mut self, username: impl ToString, password: impl ToString) -> Self {
70 self.env_vars
71 .insert("LDAP_ADMIN_USERNAME".to_owned(), username.to_string());
72 self.env_vars
73 .insert("LDAP_ADMIN_PASSWORD".to_owned(), password.to_string());
74 self
75 }
76
77 pub fn with_config_admin(mut self, username: impl ToString, password: impl ToString) -> Self {
80 self.env_vars
81 .insert("LDAP_CONFIG_ADMIN_ENABLED".to_owned(), "yes".to_owned());
82 self.env_vars.insert(
83 "LDAP_CONFIG_ADMIN_USERNAME".to_owned(),
84 username.to_string(),
85 );
86 self.env_vars.insert(
87 "LDAP_CONFIG_ADMIN_PASSWORD".to_owned(),
88 password.to_string(),
89 );
90 self
91 }
92
93 pub fn with_accesslog_settings(
97 mut self,
98 AccesslogSettings {
99 log_operations,
100 log_success,
101 log_purge,
102 log_old,
103 log_old_attribute,
104 }: AccesslogSettings,
105 ) -> Self {
106 self.env_vars
107 .insert("LDAP_ENABLE_ACCESSLOG".to_owned(), "yes".to_owned());
108 self.env_vars.insert(
109 "LDAP_ACCESSLOG_LOGOPS".to_owned(),
110 log_operations.to_string(),
111 );
112 if log_success {
113 self.env_vars
114 .insert("LDAP_ACCESSLOG_LOGSUCCESS".to_owned(), "TRUE".to_owned());
115 } else {
116 self.env_vars
117 .insert("LDAP_ACCESSLOG_LOGSUCCESS".to_owned(), "FALSE".to_owned());
118 }
119 self.env_vars.insert(
120 "LDAP_ACCESSLOG_LOGPURGE".to_owned(),
121 format!("{} {}", log_purge.0, log_purge.1),
122 );
123 self.env_vars
124 .insert("LDAP_ACCESSLOG_LOGOLD".to_owned(), log_old.to_string());
125 self.env_vars.insert(
126 "LDAP_ACCESSLOG_LOGOLDATTR".to_owned(),
127 log_old_attribute.to_string(),
128 );
129 self
130 }
131
132 pub fn with_accesslog_admin(
136 mut self,
137 username: impl ToString,
138 password: impl ToString,
139 ) -> Self {
140 self.env_vars
141 .insert("LDAP_ENABLE_ACCESSLOG".to_owned(), "yes".to_owned());
142 self.env_vars.insert(
143 "LDAP_ACCESSLOG_ADMIN_USERNAME".to_owned(),
144 username.to_string(),
145 );
146 self.env_vars.insert(
147 "LDAP_ACCESSLOG_ADMIN_PASSWORD".to_owned(),
148 password.to_string(),
149 );
150 self
151 }
152
153 pub fn with_user(mut self, username: impl ToString, password: impl ToString) -> Self {
158 self.users.push(User {
159 username: username.to_string(),
160 password: password.to_string(),
161 });
162 self
163 }
164
165 pub fn with_users<Username: ToString, Password: ToString>(
170 mut self,
171 user_password: impl IntoIterator<Item = (Username, Password)>,
172 ) -> Self {
173 for (username, password) in user_password.into_iter() {
174 self.users.push(User {
175 username: username.to_string(),
176 password: password.to_string(),
177 })
178 }
179 self
180 }
181
182 pub fn with_users_dc(mut self, user_dc: impl ToString) -> Self {
185 self.env_vars
186 .insert("LDAP_USER_DC".to_owned(), user_dc.to_string());
187 self
188 }
189
190 pub fn with_users_group(mut self, users_group: impl ToString) -> Self {
193 self.env_vars
194 .insert("LDAP_GROUP".to_owned(), users_group.to_string());
195 self
196 }
197
198 pub fn with_extra_schemas<S: ToString>(
201 mut self,
202 extra_schemas: impl IntoIterator<Item = S>,
203 ) -> Self {
204 self.env_vars
205 .insert("LDAP_ADD_SCHEMAS".to_owned(), "yes".to_owned());
206 let extra_schemas = extra_schemas
207 .into_iter()
208 .map(|s| s.to_string())
209 .collect::<Vec<String>>()
210 .join(", ");
211 self.env_vars
212 .insert("LDAP_EXTRA_SCHEMAS".to_owned(), extra_schemas);
213 self
214 }
215
216 pub fn with_allow_anon_binding(mut self, allow_anon_binding: bool) -> Self {
219 if allow_anon_binding {
220 self.env_vars
221 .insert("LDAP_ALLOW_ANON_BINDING".to_owned(), "yes".to_owned());
222 } else {
223 self.env_vars
224 .insert("LDAP_ALLOW_ANON_BINDING".to_owned(), "no".to_owned());
225 }
226 self
227 }
228
229 pub fn with_ldap_password_hash(mut self, password_hash: PasswordHash) -> Self {
232 self.env_vars
233 .insert("LDAP_PASSWORD_HASH".to_owned(), password_hash.to_string());
234 self
235 }
236
237 pub fn with_ldif_file(mut self, source: impl Into<CopyDataSource>) -> Self {
240 let n = self.copy_to_sources.len() + 1;
241
242 let container_ldif_path = format!("/ldifs/custom{n}.ldif");
243
244 self.copy_to_sources
245 .push(CopyToContainer::new(source, container_ldif_path));
246 self
247 }
248
249 pub fn with_tls(
252 mut self,
253 cert: impl Into<CopyDataSource>,
254 key: impl Into<CopyDataSource>,
255 ) -> Self {
256 self.copy_to_sources
257 .push(CopyToContainer::new(cert, "/certs/cert.crt"));
258 self.copy_to_sources
259 .push(CopyToContainer::new(key, "/certs/key.key"));
260
261 self.env_vars.insert(
262 "LDAP_TLS_CERT_FILE".to_owned(),
263 "/certs/cert.crt".to_owned(),
264 );
265 self.env_vars
266 .insert("LDAP_TLS_KEY_FILE".to_owned(), "/certs/key.key".to_owned());
267
268 self.env_vars
269 .insert("LDAP_ENABLE_TLS".to_owned(), "yes".to_owned());
270
271 self
272 }
273
274 pub fn with_cert_ca(mut self, ca: impl Into<CopyDataSource>) -> Self {
277 self.copy_to_sources
278 .push(CopyToContainer::new(ca, "/certs/ca.crt"));
279 self.env_vars
280 .insert("LDAP_TLS_CA_FILE".to_owned(), "/certs/ca.crt".to_owned());
281 self
282 }
283}
284
285#[derive(Display, FromStr, Default, Debug, Clone, Copy, Eq, PartialEq)]
287#[display("{{{}}}")] pub enum PasswordHash {
289 #[default]
290 SSHA,
293 SHA,
297 SMD5,
300 MD5,
303 CRYPT,
313 CLEARTEXT,
315}
316
317#[derive(Display, FromStr, Default, Debug, Clone, Copy, Eq, PartialEq)]
319#[display(style = "lowercase")]
320pub enum AccesslogLogOperations {
321 #[default]
323 Writes,
324 Reads,
326 Session,
328 All,
330}
331
332#[derive(Debug, Clone, Eq, PartialEq)]
334pub struct AccesslogSettings {
335 log_operations: AccesslogLogOperations,
336 log_success: bool,
337 log_purge: (String, String),
338 log_old: String,
339 log_old_attribute: String,
340}
341impl AccesslogSettings {
342 pub fn with_log_operations(mut self, log_operations: AccesslogLogOperations) -> Self {
345 self.log_operations = log_operations;
346 self
347 }
348 pub fn with_log_success(mut self, log_success: bool) -> Self {
351 self.log_success = log_success;
352 self
353 }
354 pub fn with_log_purge(
357 mut self,
358 (when_log_purge, how_often_log_purge): (impl ToString, impl ToString),
359 ) -> Self {
360 self.log_purge = (when_log_purge.to_string(), how_often_log_purge.to_string());
361 self
362 }
363 pub fn with_log_old(mut self, log_old: impl ToString) -> Self {
366 self.log_old = log_old.to_string();
367 self
368 }
369 pub fn with_log_old_attribute(mut self, log_old_attribute: impl ToString) -> Self {
372 self.log_old_attribute = log_old_attribute.to_string();
373 self
374 }
375}
376
377impl Default for AccesslogSettings {
378 fn default() -> Self {
379 Self {
380 log_operations: AccesslogLogOperations::Writes,
381 log_success: true,
382 log_purge: ("07+00:00".to_owned(), "01+00:00".to_owned()),
383 log_old: "(objectClass=*)".to_owned(),
384 log_old_attribute: "objectClass".to_owned(),
385 }
386 }
387}
388
389impl Default for OpenLDAP {
390 fn default() -> Self {
399 Self {
400 users: vec![],
401 env_vars: HashMap::new(),
402 copy_to_sources: vec![],
403 }
404 }
405}
406
407impl Image for OpenLDAP {
408 fn name(&self) -> &str {
409 NAME
410 }
411
412 fn tag(&self) -> &str {
413 TAG
414 }
415
416 fn ready_conditions(&self) -> Vec<WaitFor> {
417 vec![
420 WaitFor::message_on_stderr("** Starting slapd **"),
421 WaitFor::seconds(2), ]
423 }
424
425 fn env_vars(
426 &self,
427 ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
428 let mut vars = self.env_vars.clone();
429 let users = self
430 .users
431 .clone()
432 .into_iter()
433 .map(|u| u.username)
434 .collect::<Vec<String>>();
435 vars.insert("LDAP_USERS".to_owned(), users.join(", "));
436 let passwords = self
437 .users
438 .clone()
439 .into_iter()
440 .map(|u| u.password)
441 .collect::<Vec<String>>();
442 vars.insert("LDAP_PASSWORDS".to_owned(), passwords.join(", "));
443 vars
444 }
445
446 fn expose_ports(&self) -> &[ContainerPort] {
447 &[OPENLDAP_PORT, OPENLDAPS_PORT]
448 }
449
450 fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
451 self.copy_to_sources
452 .iter()
453 .collect::<Vec<&CopyToContainer>>()
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use ldap3::{Ldap, LdapConnAsync, LdapError, Scope, SearchEntry};
460 use testcontainers::runners::AsyncRunner;
461
462 use super::*;
463
464 async fn read_users(
465 ldap: &mut Ldap,
466 filter: &str,
467 attrs: &[&str],
468 ) -> Result<Vec<SearchEntry>, LdapError> {
469 let (rs, _res) = ldap
470 .search("ou=users,dc=example,dc=org", Scope::Subtree, filter, attrs)
471 .await?
472 .success()?;
473 let res = rs.into_iter().map(SearchEntry::construct).collect();
474 Ok(res)
475 }
476
477 async fn read_access_log(
478 ldap: &mut Ldap,
479 filter: &str,
480 attrs: &[&str],
481 ) -> Result<Vec<SearchEntry>, LdapError> {
482 let (rs, _res) = ldap
483 .search("cn=accesslog", Scope::Subtree, filter, attrs)
484 .await?
485 .success()?;
486 let res = rs.into_iter().map(SearchEntry::construct).collect();
487 Ok(res)
488 }
489
490 #[tokio::test]
491 async fn ldap_users_noauth() -> Result<(), Box<dyn std::error::Error + 'static>> {
492 let _ = pretty_env_logger::try_init();
493 let openldap_image = OpenLDAP::default()
494 .with_allow_anon_binding(true)
495 .with_user("maximiliane", "pwd1")
496 .with_user("maximus", "pwd2")
497 .with_user("lea", "pwd3");
498 let node = openldap_image.start().await?;
499
500 let connection_string = format!(
501 "ldap://{}:{}",
502 node.get_host().await?,
503 node.get_host_port_ipv4(OPENLDAP_PORT).await?,
504 );
505 let (conn, mut ldap) = LdapConnAsync::new(&connection_string).await?;
506 ldap3::drive!(conn);
507 let users = read_users(&mut ldap, "(cn=ma*)", &["cn"]).await?;
508 let users: HashMap<String, _> = users.into_iter().map(|u| (u.dn, u.attrs)).collect();
509 let expected_result_maximiliane = (
510 "cn=maximiliane,ou=users,dc=example,dc=org".to_string(),
511 HashMap::from([(
512 "cn".to_string(),
513 vec!["User1".to_string(), "maximiliane".to_string()],
514 )]),
515 );
516 let expected_result_maximus = (
517 "cn=maximus,ou=users,dc=example,dc=org".to_string(),
518 HashMap::from([(
519 "cn".to_string(),
520 vec!["User2".to_string(), "maximus".to_string()],
521 )]),
522 );
523 assert_eq!(
524 users,
525 HashMap::from([expected_result_maximus, expected_result_maximiliane])
526 );
527 ldap.unbind().await?;
528 Ok(())
529 }
530
531 #[tokio::test]
532 async fn ldap_users_simple_bind() -> Result<(), Box<dyn std::error::Error + 'static>> {
533 let _ = pretty_env_logger::try_init();
534 let openldap_image = OpenLDAP::default()
535 .with_allow_anon_binding(false)
536 .with_user("maximiliane", "pwd1");
537 let node = openldap_image.start().await?;
538
539 let connection_string = format!(
540 "ldap://{}:{}",
541 node.get_host().await?,
542 node.get_host_port_ipv4(OPENLDAP_PORT).await?,
543 );
544 let (conn, mut ldap) = LdapConnAsync::new(&connection_string).await?;
545 ldap3::drive!(conn);
546 ldap.simple_bind("cn=maximiliane,ou=users,dc=example,dc=org", "pwd1")
547 .await?
548 .success()?;
549 let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
550 assert_eq!(users.len(), 2); ldap.unbind().await?;
552 Ok(())
553 }
554
555 #[tokio::test]
556 async fn ldap_access_logs_noauth() -> Result<(), Box<dyn std::error::Error + 'static>> {
557 let _ = pretty_env_logger::try_init();
558 let openldap_image = OpenLDAP::default()
559 .with_allow_anon_binding(true)
560 .with_accesslog_settings(
561 AccesslogSettings::default().with_log_operations(AccesslogLogOperations::Reads),
562 );
563 let node = openldap_image.start().await?;
564
565 let connection_string = format!(
566 "ldap://{}:{}",
567 node.get_host().await?,
568 node.get_host_port_ipv4(OPENLDAP_PORT).await?,
569 );
570 let (conn, mut ldap) = LdapConnAsync::new(&connection_string).await?;
571 ldap3::drive!(conn);
572
573 let access = read_access_log(&mut ldap, "(reqType=search)", &["*"]).await?;
574 assert_eq!(access.len(), 0, "no search until now");
575
576 let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
577 assert_eq!(users.len(), 3, "cn=readers should be read");
578
579 let access = read_access_log(&mut ldap, "(reqType=search)", &["*"]).await?;
580 assert_eq!(access.len(), 1, "access log contains 1xread_users");
581
582 ldap.unbind().await?;
583 Ok(())
584 }
585
586 #[tokio::test]
587 async fn ldap_with_ldif() -> Result<(), Box<dyn std::error::Error + 'static>> {
588 let _ = pretty_env_logger::try_init();
589 let ldif = r#"
590version: 1
591
592# Entry 1: dc=example,dc=org
593dn: dc=example,dc=org
594objectClass: top
595objectClass: domain
596dc: example
597
598# Entry 2: ou=users,dc=frauscher,dc=test
599dn: ou=users,dc=example,dc=org
600objectclass: organizationalUnit
601objectclass: top
602ou: users
603
604# Entry 3: cn=maximiliane,dc=example,dc=org
605dn: cn=maximiliane,ou=users,dc=example,dc=org
606cn: maximiliane
607gidnumber: 501
608givenname: Florian
609homedirectory: /home/users/maximiliane
610objectclass: inetOrgPerson
611objectclass: posixAccount
612objectclass: top
613sn: Maximiliane
614uid: maximiliane
615uidnumber: 1001
616userpassword: {SSHA}1vtVpqX6Y77jAVs/1uTd/rHS8YRYEh/9
617"#;
618
619 let openldap_image = OpenLDAP::default().with_ldif_file(ldif.to_string().into_bytes());
620
621 let node = openldap_image.start().await?;
622
623 let connection_string = format!(
624 "ldap://{}:{}",
625 node.get_host().await?,
626 node.get_host_port_ipv4(OPENLDAP_PORT).await?,
627 );
628 let (conn, mut ldap) = LdapConnAsync::new(&connection_string).await?;
629 ldap3::drive!(conn);
630 ldap.simple_bind("cn=maximiliane,ou=users,dc=example,dc=org", "pwd1")
631 .await?
632 .success()?;
633 let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
634 assert_eq!(users.len(), 1); ldap.unbind().await?;
636 Ok(())
637 }
638
639 #[tokio::test]
640 async fn ldap_secure() -> Result<(), Box<dyn std::error::Error + 'static>> {
641 let _ = pretty_env_logger::try_init();
642
643 let root_ca = r#"-----BEGIN CERTIFICATE-----
644MIIDazCCAlOgAwIBAgIUacPFMoNlemFtn974hn0x4c+ssGowDQYJKoZIhvcNAQEL
645BQAwRTELMAkGA1UEBhMCQVQxDTALBgNVBAoMBFRlc3QxFjAUBgNVBAsMDVRlc3Rj
646b250YWluZXIxDzANBgNVBAMMBnJvb3RDQTAeFw0yNDA5MjUxODI0MDFaFw00NDA5
647MjAxODI0MDFaMEUxCzAJBgNVBAYTAkFUMQ0wCwYDVQQKDARUZXN0MRYwFAYDVQQL
648DA1UZXN0Y29udGFpbmVyMQ8wDQYDVQQDDAZyb290Q0EwggEiMA0GCSqGSIb3DQEB
649AQUAA4IBDwAwggEKAoIBAQC4+4uW9PE5HQGWtn4/P1wPm11z/UevNGveDEUvwcDz
650oEZlfRLTihoVfc7EZGUF8/6TS2rkp1qz2yrDDP4o7a7l0Om22vCshsWlhhqQ1mON
651tiXnOWXqKjon6NZatvYnQVvUwm8BuSi4HR4xNM8N9BF81rhFj3tbam3lylJign/3
652sFsxUg2C3mAaDoolTFslBPPnKHW2EmSw04kgC6XhWGXqpAbWbL5h2rlOE/WoLTkl
653f+EpILsCyEApNbVLXh+B9+IrJvNqX6yuFL7zzJhefOa1L1sAG0bNc9nAwuwyDcDu
654OzxULkv0JGK2Dob6NL74NSB96kPLbZi/FRIHwhBgmUlPAgMBAAGjUzBRMB0GA1Ud
655DgQWBBQ2Bcju3oNHso17GQFoKd5/46OsnjAfBgNVHSMEGDAWgBQ2Bcju3oNHso17
656GQFoKd5/46OsnjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAP
657za/O0NzyLZjjOoBsDKyJzvgHv/Ls8yBRSmwAbJoXqT1F4F39px5ZVOdmruXeJsG1
658PIaLb2/4oPoSgqLfiPUMAJelTjD/CVpfFthiorCgYPSIIhkjgc6jdhCez67gotFE
659nBITxIRg47yAmCA+0+/YtTnul1deB9r2cuXeHVTPfUVphDsKSGVLJS0TK1iIL/PZ
660k/bSZzdk/i80spmFS3W/fLHPWFUbio1r0CBpbibQNl19x8uHz+J7L1kmW7gofyd9
661w4a3nYIMqapx5KGgGqI3lyc/ePet2JhUabbu7rQx6oEHJaaco3qip2t6pS0WtDcG
662B5tEQNlAfsx/Rfi4JUNi
663-----END CERTIFICATE-----
664"#;
665 let key = r#"-----BEGIN PRIVATE KEY-----
666MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDB8KR+QtENXXI0
667+BWZ/rD2nDfRi7v6dDLpD1yuVLFbBgVMDisf8CoqZP67x9lW5sRz/CXLjY+If1Vi
668xFxcwURzVBWmrMBLbEgDnsyX0GhgCdZTJM3eBTXwCN77bH/xSkZ6Dg1AV/i/ltAI
669TvMlE5Azrd/0IePWEQYtQYUQMlJHcxrDex3sKZ/oM6ohMl1sbkNDV+Ncatl7bIel
670RDyHE1SgExFFL2qWg2CeHJHPliq/0osp1cU99SCtaalxwisnih0GMswIf++1s+r6
671/CGRI+5xXOd1wtPwcjFxdVnSuhbFxniz9fPaPXw0NoVBksRcJNri2RHDZpW7nK+D
672OMcvWtxxAgMBAAECggEANQPOQ4GKWgfwX1Btv0HjKBa+H3b+NNGs1Q7Q/ArEzKgR
673rJ+25C0nqZ0gET7pR5sfmsETp9gTo3GDatNYmDZwusIChSR2EGgSK4MuVFWxIoet
6744d6OtCFihDI4miwnsVLnfxf2QV+K7PyR86N5TepSIf5m2PqmqG7Q7HAbqrjGyybO
675te4IIYUox2OirICfNMdy6ROj3c+oEiXu5eEpxbl01WzSU7rO4fMdfc0O/cocg2rr
676njBXD0utCiUw0M6EaisYzAg5ughP02enmxGzGwBT+lr2AQHG3Mk++PICRxsiBIv0
677T0o6Q3mIR4YMNDt7dZVz8mjL7lx1iuczM5CUbRnhzQKBgQDxTubdOmTidd1WxRJ5
6784t19Ga604YVG8c97hZ7fpNL0fTY/JcxkEN02uhHgA9fmBhE2vRus82x+iLKjcNBY
679ZKzM2PaRsjK2g9UNi829Zii/VjZJX1QwipO0EQt0Rruf+QtWXL47UHKTxpRfLrFN
680zaXrVM15uq/pdhmovj+LQ4NplwKBgQDNv3JQVXBImy9/5h2xPwVlkjXzQdD0PTEs
681emnLRfnctyqAzmtgTsbS1tDhoCwm5TuI6MMbrtnojSQKRr68BDfhvnm4TBHWYrS1
6820BqY+P1XkJH4jmIQsRpS/ZnDlB50+aiAnuMahui0mUT71/upGS277Zqr+u7Gkt2b
683fFeWzu3bNwKBgFiU+FbZ6tLfJaOGsKOhzmDwHpwz9XL3rYzQnmPG49HwbQt9WqyZ
684LDu8zncHsie0rnkDrrcsnPVORRWOgk0QmAaS1uDhI5CwkHNqkNooOGkUwtToc8Vl
685+Zaucx/6H0I4cBsB7KtlesoYqbrPLzM6fOAIv20iRRVUz1KMlFMRM5p9AoGBAJJr
686Tv/Kfbi974S2j6Tms4GAFrLBwOE/dvIvP4CwkMs48p9txs5n4WiEBWy73w/jDIY3
687FzppKZwsbVx+0hfdbKNTOS4lvH/0CKRmr7bzYt9g+/CF61Xzo0cyQK4Fh9M5JGg8
688KmRjY9G6TXRoVSkWyQw3YF5JmoloVRrk1zR0mKLrAoGBAKPJmagtBVmTvKdYFhs0
689X6au0mG5xN4HnkdvX1sGgeg+DgJ/dCa4LA3lZagUhDjupR0QgaaYVZhyFe6lDVck
690lOB14QZNoFr+xwiSDg4oM4/TQXualO/4nZxzUynbZNdNuvhQ8mRdeu5jqs363deP
691bCWwJaA6QQpNSitVrzg5XbRQ
692-----END PRIVATE KEY-----
693"#;
694 let cert = r#"-----BEGIN CERTIFICATE-----
695MIIDqzCCApOgAwIBAgIUJArHsm2jdPlyOXWC7TWdFxYD19kwDQYJKoZIhvcNAQEL
696BQAwRTELMAkGA1UEBhMCQVQxDTALBgNVBAoMBFRlc3QxFjAUBgNVBAsMDVRlc3Rj
697b250YWluZXIxDzANBgNVBAMMBnJvb3RDQTAeFw0yNDA5MjUxODI0MDJaFw00NDA5
698MjAxODI0MDJaME8xCzAJBgNVBAYTAkFUMQ0wCwYDVQQKDARUZXN0MRYwFAYDVQQL
699DA1UZXN0Y29udGFpbmVyMRkwFwYDVQQDDBBsZGFwLmV4YW1wbGUub3JnMIIBIjAN
700BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwfCkfkLRDV1yNPgVmf6w9pw30Yu7
701+nQy6Q9crlSxWwYFTA4rH/AqKmT+u8fZVubEc/wly42PiH9VYsRcXMFEc1QVpqzA
702S2xIA57Ml9BoYAnWUyTN3gU18Aje+2x/8UpGeg4NQFf4v5bQCE7zJROQM63f9CHj
7031hEGLUGFEDJSR3Maw3sd7Cmf6DOqITJdbG5DQ1fjXGrZe2yHpUQ8hxNUoBMRRS9q
704loNgnhyRz5Yqv9KLKdXFPfUgrWmpccIrJ4odBjLMCH/vtbPq+vwhkSPucVzndcLT
7058HIxcXVZ0roWxcZ4s/Xz2j18NDaFQZLEXCTa4tkRw2aVu5yvgzjHL1rccQIDAQAB
706o4GIMIGFMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAeBgNV
707HREEFzAVgglsb2NhbGhvc3SCCG9wZW5sZGFwMB0GA1UdDgQWBBTOko7NFMj7/fd/
708KGvfbYezhjmH8zAfBgNVHSMEGDAWgBQ2Bcju3oNHso17GQFoKd5/46OsnjANBgkq
709hkiG9w0BAQsFAAOCAQEAlud2ST43+Q2Sa+VHS8Tio1M76+AdNj1dmQHYsFN7Vm91
710cAEOFOO8y/oSqTZrIuxenFCIsMeAAVOEZ7BjcpzX50ncHAYDu2szpmTscvujNoSs
7111qvbfRC1aL8bky4XECct7Md1h7TTN/pY0E+6b1wI0gyHkCeuiaOfeq7I+lUogIzL
712SpuBTQvi59BdeLyTXImg8WCSKoLrZljdaEjCZdM51FWFvY2WdW1NE/ahniJpkGv5
713hcDj6qNPn8FHCLxzOs1HUucyncxbS9z6I91WaFWXWu0DH90lMA8gedyXZr6YOnkg
714H32P9zbIKaSiPxFg5JVRW5hpQWUI1dYr3CpKP4i98w==
715-----END CERTIFICATE-----
716"#;
717
718 let openldap_image = OpenLDAP::default()
719 .with_allow_anon_binding(false)
720 .with_user("maximiliane", "pwd1")
721 .with_tls(cert.to_string().into_bytes(), key.to_string().into_bytes())
722 .with_cert_ca(root_ca.to_string().into_bytes());
723 let node = openldap_image.start().await?;
724
725 let connection_string = format!(
726 "ldaps://{}:{}",
727 node.get_host().await?,
728 node.get_host_port_ipv4(OPENLDAPS_PORT).await?,
729 );
730
731 let mut builder = native_tls::TlsConnector::builder();
732 let root_ca = native_tls::Certificate::from_pem(root_ca.as_bytes())?;
733 let connector = builder.add_root_certificate(root_ca).build()?;
734
735 let settings = ldap3::LdapConnSettings::new().set_connector(connector);
736 let (conn, mut ldap) =
737 ldap3::LdapConnAsync::with_settings(settings, &connection_string).await?;
738
739 ldap3::drive!(conn);
740 ldap.simple_bind("cn=maximiliane,ou=users,dc=example,dc=org", "pwd1")
741 .await?
742 .success()?;
743 let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
744 assert_eq!(users.len(), 2); ldap.unbind().await?;
746 Ok(())
747 }
748}