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}