ldap_test_server/
lib.rs

1//! This crate allow running isolated OpenLDAP (slapd) server in integration tests.
2//!
3//! # Examples
4//!
5//! ```
6//! use ldap_test_server::{LdapServerConn, LdapServerBuilder};
7//!
8//! # #[tokio::main(flavor = "current_thread")]
9//! # async fn main() {
10//! let server: LdapServerConn = LdapServerBuilder::new("dc=planetexpress,dc=com")
11//!         // add LDIF to database before LDAP server is started
12//!         .add(1, "dn: dc=planetexpress,dc=com
13//! objectclass: dcObject
14//! objectclass: organization
15//! o: Planet Express
16//! dc: planetexpress
17//!
18//! dn: ou=people,dc=planetexpress,dc=com
19//! objectClass: top
20//! objectClass: organizationalUnit
21//! description: Planet Express crew
22//! ou: people")
23//!         // init databases and started LDAP server
24//!         .run()
25//!         .await;
26//!
27//! // Add entity to running LDAP server
28//! server.add(r##"dn: cn=Turanga Leela,ou=people,dc=planetexpress,dc=com
29//! objectClass: inetOrgPerson
30//! objectClass: organizationalPerson
31//! objectClass: person
32//! objectClass: top
33//! cn: Turanga Leela
34//! sn: Turanga
35//! givenName: Leela"##).await;
36//! # }
37//! ```
38//!
39#![warn(missing_docs)]
40use dircpy::copy_dir;
41use std::convert::AsRef;
42use std::path::Path;
43use tempfile::TempDir;
44use tokio::process::{Child, Command};
45use tokio::task;
46use tracing::{debug, warn};
47
48mod builder;
49
50pub use builder::LdapServerBuilder;
51
52/// Connection to running LDAP server
53#[derive(Debug)]
54pub struct LdapServerConn {
55    url: String,
56    host: String,
57    port: u16,
58    ssl_url: String,
59    ssl_port: u16,
60    ssl_cert_pem: String,
61    #[allow(unused)]
62    dir: TempDir,
63    base_dn: String,
64    root_dn: String,
65    root_pw: String,
66    server: Child,
67}
68
69impl LdapServerConn {
70    /// Return URL (schema=ldap, host and port) to this LDAP server
71    pub fn url(&self) -> &str {
72        &self.url
73    }
74
75    /// Hostname of this LDAP server
76    pub fn host(&self) -> &str {
77        &self.host
78    }
79
80    /// TCP port number of this LDAP server
81    pub fn port(&self) -> u16 {
82        self.port
83    }
84
85    /// Return URL (schema=ldaps, host and port) to this LDAP server
86    pub fn ssl_url(&self) -> &str {
87        &self.ssl_url
88    }
89
90    /// SSL (ldaps) TCP port number of this LDAP server
91    pub fn ssl_port(&self) -> u16 {
92        self.ssl_port
93    }
94
95    /// PEM Certificate for ssl port
96    pub fn ssl_cert_pem(&self) -> &str {
97        &self.ssl_cert_pem
98    }
99
100    /// Base DN of this LDAP server
101    pub fn base_dn(&self) -> &str {
102        &self.base_dn
103    }
104
105    /// Administrator account of this LDAP server
106    pub fn root_dn(&self) -> &str {
107        &self.root_dn
108    }
109
110    /// Password for administrator server of this LDAP server
111    pub fn root_pw(&self) -> &str {
112        &self.root_pw
113    }
114
115    /// LDAP server directory location
116    pub fn server_dir(&self) -> &Path {
117        self.dir.path()
118    }
119
120    /// Clone LDAP server files to new location
121    pub async fn clone_to_dir<P: AsRef<Path>>(&self, desc: P) {
122        let src = self.dir.path().to_path_buf();
123        let dst = desc.as_ref().to_path_buf();
124        task::spawn_blocking(move || {
125            copy_dir(&src, &dst).unwrap();
126        })
127        .await
128        .unwrap();
129    }
130
131    /// Apply LDIF from text
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// # use ldap_test_server::LdapServerBuilder;
137    /// #
138    /// # #[tokio::main(flavor = "current_thread")]
139    /// # async fn main() {
140    /// # let server = LdapServerBuilder::new("dc=planetexpress,dc=com")
141    /// #     .add(1, "dn: dc=planetexpress,dc=com
142    /// # objectclass: dcObject
143    /// # objectclass: organization
144    /// # o: Planet Express
145    /// # dc: planetexpress")
146    /// #     .run().await;
147    /// #
148    /// server.add("dn: cn=Philip J. Fry,dc=planetexpress,dc=com
149    /// objectClass: inetOrgPerson
150    /// objectClass: organizationalPerson
151    /// objectClass: person
152    /// objectClass: top
153    /// cn: Philip J. Fry
154    /// givenName: Philip
155    /// sn: Fry").await;
156    /// # }
157    /// ```
158    pub async fn add(&self, ldif_text: &str) -> &Self {
159        let tmp_ldif = self.dir.path().join("tmp.ldif");
160        tokio::fs::write(&tmp_ldif, ldif_text).await.unwrap();
161        self.add_file(tmp_ldif).await
162    }
163
164    /// Apply LDIF from file
165    pub async fn add_file<P: AsRef<Path>>(&self, file: P) -> &Self {
166        self.load_ldif_file("ldapadd", file, self.root_dn(), self.root_pw())
167            .await
168    }
169
170    /// Apply modification LDIF from text
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// # use ldap_test_server::LdapServerBuilder;
176    /// #
177    /// # #[tokio::main(flavor = "current_thread")]
178    /// # async fn main() {
179    /// # let server = LdapServerBuilder::new("dc=planetexpress,dc=com")
180    /// #     .add(1, "dn: dc=planetexpress,dc=com
181    /// # objectclass: dcObject
182    /// # objectclass: organization
183    /// # o: Planet Express
184    /// # dc: planetexpress")
185    /// #     .run().await;
186    /// #
187    /// # server.add("dn: cn=Philip J. Fry,dc=planetexpress,dc=com
188    /// # objectClass: inetOrgPerson
189    /// # objectClass: organizationalPerson
190    /// # objectClass: person
191    /// # objectClass: top
192    /// # cn: Philip J. Fry
193    /// # givenName: Philip
194    /// # sn: Fry").await;
195    /// #
196    /// server.modify("dn: cn=Philip J. Fry,dc=planetexpress,dc=com
197    /// changetype: modify
198    /// add: displayName
199    /// displayName: Philip J. Fry").await;
200    /// # }
201    /// ```
202    pub async fn modify(&self, ldif_text: &str) -> &Self {
203        let tmp_ldif = self.dir.path().join("tmp.ldif");
204        tokio::fs::write(&tmp_ldif, ldif_text).await.unwrap();
205        self.modify_file(tmp_ldif).await
206    }
207
208    /// Apply modification LDIF from file
209    pub async fn modify_file<P: AsRef<Path>>(&self, file: P) -> &Self {
210        self.load_ldif_file("ldapmodify", file, self.root_dn(), self.root_pw())
211            .await
212    }
213
214    /// Apply deletion LDIF from text
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// # use ldap_test_server::LdapServerBuilder;
220    /// #
221    /// # #[tokio::main(flavor = "current_thread")]
222    /// # async fn main() {
223    /// # let server = LdapServerBuilder::new("dc=planetexpress,dc=com")
224    /// #     .add(1, "dn: dc=planetexpress,dc=com
225    /// # objectclass: dcObject
226    /// # objectclass: organization
227    /// # o: Planet Express
228    /// # dc: planetexpress")
229    /// #     .run().await;
230    /// #
231    /// # server.add("dn: cn=Philip J. Fry,dc=planetexpress,dc=com
232    /// # objectClass: inetOrgPerson
233    /// # objectClass: organizationalPerson
234    /// # objectClass: person
235    /// # objectClass: top
236    /// # cn: Philip J. Fry
237    /// # givenName: Philip
238    /// # sn: Fry").await;
239    /// #
240    /// server.delete("dn: cn=Philip J. Fry,dc=planetexpress,dc=com
241    /// changetype: delete").await;
242    /// # }
243    /// ```
244    pub async fn delete(&self, ldif_text: &str) -> &Self {
245        let tmp_ldif = self.dir.path().join("tmp.ldif");
246        tokio::fs::write(&tmp_ldif, ldif_text).await.unwrap();
247        self.modify_file(tmp_ldif).await
248    }
249
250    /// Apply deletion LDIF from file
251    pub async fn delete_file<P: AsRef<Path>>(&self, file: P) -> &Self {
252        self.load_ldif_file("ldapdelete", file, self.root_dn(), self.root_pw())
253            .await
254    }
255
256    async fn load_ldif_file<P: AsRef<Path>>(
257        &self,
258        command: &str,
259        file: P,
260        binddn: &str,
261        password: &str,
262    ) -> &Self {
263        let file = file.as_ref();
264
265        let output = Command::new(command)
266            .args(["-x", "-D", binddn, "-w", password, "-H", self.url(), "-f"])
267            .arg(file)
268            .output()
269            .await
270            .expect("failed to load ldap file");
271
272        if !output.status.success() {
273            panic!(
274                "{command} command exited with error {}, stdout: {}, stderr: {} on file {}",
275                output.status,
276                String::from_utf8_lossy(&output.stdout),
277                String::from_utf8_lossy(&output.stderr),
278                file.display()
279            );
280        }
281
282        self
283    }
284}
285
286impl Drop for LdapServerConn {
287    fn drop(&mut self) {
288        if let Err(e) = self.server.start_kill() {
289            warn!(
290                "failed to kill slapd server: {}, pid: {:?}",
291                e,
292                self.server.id()
293            );
294        } else {
295            debug!("killed slapd server pid: {:?}", self.server.id());
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use std::time::Instant;
304
305    #[tokio::test]
306    async fn run_slapd() {
307        let started = Instant::now();
308
309        let server = LdapServerBuilder::new("dc=planetexpress,dc=com")
310            .add_system_file(0, "pmi.ldif")
311            .add(
312                1,
313                "dn: dc=planetexpress,dc=com
314objectclass: dcObject
315objectclass: organization
316o: Planet Express
317dc: planetexpress
318
319dn: ou=people,dc=planetexpress,dc=com
320objectClass: top
321objectClass: organizationalUnit
322description: Planet Express crew
323ou: people",
324            )
325            .add_file(1, concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fry.ldif"))
326            .run()
327            .await;
328
329        server
330            .add(
331                "dn: cn=Amy Wong+sn=Kroker,ou=people,dc=planetexpress,dc=com
332objectClass: top
333objectClass: person
334objectClass: organizationalPerson
335objectClass: inetOrgPerson
336cn: Amy Wong
337sn: Kroker
338description: Human
339givenName: Amy
340mail: amy@planetexpress.com
341ou: Intern
342uid: amy",
343            )
344            .await;
345
346        println!("Server started in {} ms", started.elapsed().as_millis());
347    }
348}