1#![deny(unknown_lints)]
2#![deny(renamed_and_removed_lints)]
3#![forbid(unsafe_code)]
4#![deny(deprecated)]
5#![forbid(private_interfaces)]
6#![forbid(private_bounds)]
7#![forbid(non_fmt_panics)]
8#![deny(unreachable_code)]
9#![deny(unreachable_patterns)]
10#![forbid(unused_doc_comments)]
11#![forbid(unused_must_use)]
12#![deny(while_true)]
13#![deny(unused_parens)]
14#![deny(redundant_semicolons)]
15#![deny(non_ascii_idents)]
16#![deny(confusable_idents)]
17#![warn(missing_docs)]
18#![warn(clippy::missing_docs_in_private_items)]
19#![warn(clippy::cargo_common_metadata)]
20#![warn(rustdoc::missing_crate_level_docs)]
21#![deny(rustdoc::broken_intra_doc_links)]
22#![warn(missing_debug_implementations)]
23#![deny(clippy::mod_module_files)]
24#![doc = include_str!("../README.md")]
25
26use lazy_static::lazy_static;
27
28use diff::{Diff, VecDiffType};
29
30use chumsky::Parser;
31use ldap_types::basic::{ChumskyError, LDAPEntry, LDAPOperation, OIDWithLength, RootDSE};
32use ldap_types::schema::{
33 attribute_type_parser, ldap_syntax_parser, matching_rule_parser, matching_rule_use_parser,
34 object_class_parser, AttributeType, LDAPSchema, LDAPSyntax, MatchingRule, MatchingRuleUse,
35 ObjectClass,
36};
37
38use ldap3::exop::{WhoAmI, WhoAmIResp};
39use ldap3::result::SearchResult;
40use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
41use native_tls::{Certificate, Identity, TlsConnector};
42use oid::ObjectIdentifier;
43
44use std::collections::{HashMap, HashSet};
45use std::fmt::Debug;
46use std::fmt::Display;
47
48use openssl::pkcs12::Pkcs12;
49use openssl::pkey::PKey;
50use openssl::x509::X509;
51
52use std::fs::File;
53use std::io::Read;
54use std::path::Path;
55
56use regex::Regex;
57
58use dirs2::home_dir;
59
60use tracing::instrument;
61
62use derive_builder::Builder;
63
64use serde::Deserialize;
65
66use std::convert::TryInto;
67
68use thiserror::Error;
69
70pub fn noop_control() -> ldap3::controls::RawControl {
80 ldap3::controls::RawControl {
81 ctype: "1.3.6.1.4.1.4203.666.5.2".to_string(),
82 crit: true,
83 val: None,
84 }
85}
86
87#[derive(Debug, Clone, Error)]
89pub enum ScopeParserError {
90 #[error("Could not parse {0} as an ldap scope")]
92 CouldNotParseAsScope(String),
93}
94
95pub fn parse_scope(src: &str) -> Result<ldap3::Scope, ScopeParserError> {
98 match src {
99 "base" => Ok(ldap3::Scope::Base),
100 "one" => Ok(ldap3::Scope::OneLevel),
101 "sub" => Ok(ldap3::Scope::Subtree),
102 s => Err(ScopeParserError::CouldNotParseAsScope(s.to_string())),
103 }
104}
105
106#[derive(Debug, Clone, Builder, Deserialize)]
109pub struct ConnectParameters {
110 ca_cert_path: std::string::String,
112 client_cert_path: std::string::String,
114 client_key_path: std::string::String,
116 pub url: std::string::String,
118}
119
120#[derive(Debug, Error)]
122pub enum OpenLdapConnectParameterError {
123 #[error("regex error: {0}")]
125 RegexError(#[from] regex::Error),
126 #[error("I/O error: {0}")]
128 IOError(#[from] std::io::Error),
129}
130
131#[instrument(skip(builder))]
134pub fn openldap_connect_parameters(
135 builder: &mut ConnectParametersBuilder,
136) -> Result<&mut ConnectParametersBuilder, OpenLdapConnectParameterError> {
137 let ldap_rc_content;
138 let ldap_conf_content;
139 if let Some(d) = home_dir() {
140 let mut ldap_rc_filename = d;
141 ldap_rc_filename.push(".ldaprc");
142 if ldap_rc_filename.exists() {
143 tracing::debug!("Using .ldaprc at {:?}", ldap_rc_filename);
144 ldap_rc_content = std::fs::read_to_string(ldap_rc_filename)?;
145
146 let ca_cert_re = Regex::new(r"^TLS_CACERT *(.*)$")?;
147 let client_cert_re = Regex::new(r"^TLS_CERT *(.*)$")?;
148 let client_key_re = Regex::new(r"^TLS_KEY *(.*)$")?;
149 for line in ldap_rc_content.lines() {
150 if let Some(caps) = ca_cert_re.captures(line) {
151 let ca_cert_path = caps.get(1).unwrap().as_str();
152 tracing::debug!("Extracted .ldaprc TLS_CACERT value {}", ca_cert_path);
153 builder.ca_cert_path(ca_cert_path.to_string());
154 }
155
156 if let Some(caps) = client_cert_re.captures(line) {
157 let client_cert_path = caps.get(1).unwrap().as_str();
158 tracing::debug!("Extracted .ldaprc TLS_CERT value {}", client_cert_path);
159 builder.client_cert_path(client_cert_path.to_string());
160 }
161
162 if let Some(caps) = client_key_re.captures(line) {
163 let client_key_path = caps.get(1).unwrap().as_str();
164 tracing::debug!("Extracted .ldaprc TLS_KEY value {}", client_key_path);
165 builder.client_key_path(client_key_path.to_string());
166 }
167 }
168 }
169
170 let mut ldap_conf_filename = Path::new("/etc/ldap/ldap.conf");
171 if !ldap_conf_filename.exists() {
172 ldap_conf_filename = Path::new("/etc/openldap/ldap.conf");
173 }
174 if ldap_conf_filename.exists() {
175 tracing::debug!("Using ldap.conf at {:?}", ldap_conf_filename);
176 ldap_conf_content = std::fs::read_to_string(ldap_conf_filename)?;
177
178 let uri_re = Regex::new(r"^URI *(.*)$")?;
179 for line in ldap_conf_content.lines() {
180 if let Some(caps) = uri_re.captures(line) {
181 let url = caps.get(1).unwrap().as_str();
182 tracing::debug!("Extracted ldap.conf URI value {}", url);
183 builder.url(url.to_string());
184 }
185 }
186 }
187 }
188 Ok(builder)
189}
190
191#[instrument(skip(builder))]
195pub fn default_connect_parameters(
196 builder: &mut ConnectParametersBuilder,
197) -> &mut ConnectParametersBuilder {
198 if builder.ca_cert_path.is_none() {
199 builder.ca_cert_path("ca.crt".to_string());
200 }
201 if builder.client_cert_path.is_none() {
202 builder.client_cert_path("client.crt".to_string());
203 }
204 if builder.client_key_path.is_none() {
205 builder.client_key_path("client.key".to_string());
206 }
207 builder
208}
209
210#[derive(Debug, Error)]
212pub enum TomlConfigError {
213 #[error("I/O error: {0}")]
215 IOError(#[from] std::io::Error),
216 #[error("Toml deserialization error: {0}")]
218 TomlError(#[from] toml::de::Error),
219}
220
221#[instrument]
223pub fn toml_connect_parameters(
224 filename: std::path::PathBuf,
225) -> Result<ConnectParameters, TomlConfigError> {
226 let config = std::fs::read_to_string(filename)?;
227 let result: ConnectParameters = toml::from_str(&config)?;
228
229 Ok(result)
230}
231
232#[derive(Debug, Error)]
234pub enum ConnectError {
235 #[error("Parameters builder error: {0}")]
238 ParametersBuilderError(#[from] ConnectParametersBuilderError),
239 #[error("Error retrieving OpenLDAP connect parameters: {0}")]
241 OpenLdapConnectParameterError(#[from] OpenLdapConnectParameterError),
242 #[error("I/O error: {0}")]
244 IOError(#[from] std::io::Error),
245 #[error("Native TLS error: {0}")]
247 NativeTLSError(#[from] native_tls::Error),
248 #[error("ldap3 Ldap error: {0}")]
250 LdapError(#[from] ldap3::LdapError),
251 #[error("regex error: {0}")]
253 RegexError(#[from] regex::Error),
254 #[error("openssl error: {0}")]
256 OpenSSLError(#[from] openssl::error::ErrorStack),
257}
258
259#[instrument]
262pub async fn connect() -> Result<(Ldap, std::string::String), ConnectError> {
263 let mut builder = ConnectParametersBuilder::default();
264 openldap_connect_parameters(&mut builder)?;
265 match builder.build() {
266 Ok(result) => connect_with_parameters(result).await,
267 Err(err_msg) => {
268 tracing::error!(
269 "Building of ConnectParameters based on OpenLDAP config files failed: {}",
270 err_msg
271 );
272 let builder = default_connect_parameters(&mut builder);
273 match builder.build() {
274 Ok(result) => connect_with_parameters(result).await,
275 Err(err) => {
276 tracing::error!("Building of ConnectParameters based on OpenLDAP config files and substituting default values for missing values failed: {}", err);
277 Err(ConnectError::ParametersBuilderError(err))
278 }
279 }
280 }
281 }
282}
283
284#[instrument]
286pub async fn connect_with_parameters(
287 connect_parameters: ConnectParameters,
288) -> Result<(Ldap, std::string::String), ConnectError> {
289 let mut client_cert_contents = Vec::new();
290 {
291 let mut file = File::open(connect_parameters.client_cert_path)?;
292 file.read_to_end(&mut client_cert_contents)?;
293 }
294 let client_cert = X509::from_pem(&client_cert_contents)?;
295 let mut client_key_contents = Vec::new();
296 {
297 let mut file = File::open(connect_parameters.client_key_path)?;
298 file.read_to_end(&mut client_key_contents)?;
299 }
300 let client_key = PKey::private_key_from_pem(&client_key_contents)?;
301 let p12_password = "client";
302 let p12 = Pkcs12::builder()
303 .name("client")
304 .pkey(&client_key)
305 .cert(&client_cert)
306 .build2(p12_password)?;
307 let p12_contents = p12.to_der()?;
308 let mut ca_cert_contents = Vec::new();
309 {
310 let mut file = File::open(connect_parameters.ca_cert_path)?;
311 file.read_to_end(&mut ca_cert_contents)?;
312 }
313 let identity = Identity::from_pkcs12(&p12_contents, p12_password)?;
314 let ca_certificate = Certificate::from_pem(&ca_cert_contents)?;
315 let connector = TlsConnector::builder()
316 .identity(identity)
317 .add_root_certificate(ca_certificate)
318 .build()?;
319 let ldap_settings = LdapConnSettings::new().set_connector(connector);
320 let (ldap_conn_async, mut ldap) =
321 LdapConnAsync::with_settings(ldap_settings, &connect_parameters.url.clone()).await?;
322 ldap3::drive!(ldap_conn_async);
323 ldap.sasl_external_bind().await?;
324 let (exop, _res) = ldap.extended(WhoAmI).await?.success()?;
325 let who_am_i: WhoAmIResp = exop.parse();
326 let re = Regex::new(r"^.*,ou=[a-z]+,")?;
327 let base_dn = re.replace_all(&who_am_i.authzid, "").to_string();
328 Ok((ldap, base_dn))
329}
330
331#[derive(Debug, Error)]
333pub enum LdapOperationError {
334 #[error("ldap3 Ldap error: {0}")]
336 LdapError(#[from] ldap3::LdapError),
337 #[error("OID error: {0}")]
339 OIDError(#[from] OIDError),
340}
341
342pub async fn ldap_search<'a, S: AsRef<str> + Clone + Display + Debug + Send + Sync>(
345 ldap: &mut Ldap,
346 base: &str,
347 scope: Scope,
348 filter: &str,
349 attrs: Vec<S>,
350) -> Result<Box<dyn Iterator<Item = SearchEntry> + 'a>, LdapOperationError> {
351 let search_result = ldap.search(base, scope, filter, attrs.clone()).await?;
352 let SearchResult(_rs, res) = &search_result;
353 if res.rc != 0 {
354 tracing::debug!(
355 "Non-zero return code {} in LDAP query\n base: {}\n scope: {:?}\n filter: {}\n attrs: {:#?}",
356 res.rc,
357 base,
358 scope,
359 filter,
360 attrs
361 );
362 tracing::debug!(
363 "ldapsearch -Q -LLL -o ldif-wrap=no -b '{}' -s {} '{}' {}",
364 base,
365 format!("{:?}", scope).to_lowercase(),
366 filter,
367 itertools::join(attrs.iter(), " ")
368 );
369 }
370 let (rs, _res) = search_result.success()?;
371 Ok(Box::new(rs.into_iter().map(SearchEntry::construct)))
372}
373
374#[derive(Debug)]
377pub struct OIDError(oid::ObjectIdentifierError);
378
379impl Display for OIDError {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 write!(f, "Error parsing OID: {:?}", self.0)
382 }
383}
384
385impl std::error::Error for OIDError {
386 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
387 None
388 }
389}
390
391#[instrument(skip(ldap))]
393pub async fn query_root_dse(ldap: &mut Ldap) -> Result<Option<RootDSE>, LdapOperationError> {
394 let mut it = ldap_search(
395 ldap,
396 "",
397 Scope::Base,
398 "(objectClass=*)",
399 vec![
400 "supportedLDAPVersion",
401 "supportedControl",
402 "supportedExtension",
403 "supportedFeatures",
404 "supportedSASLMechanisms",
405 "configContext",
406 "namingContexts",
407 "subschemaSubentry",
408 ],
409 )
410 .await?;
411 if let Some(entry) = it.next() {
412 let supported_ldap_version = entry
413 .attrs
414 .get("supportedLDAPVersion")
415 .unwrap()
416 .first()
417 .unwrap();
418 let supported_controls = entry.attrs.get("supportedControl").unwrap();
419 let supported_extensions = entry.attrs.get("supportedExtension").unwrap();
420 let supported_features = entry.attrs.get("supportedFeatures").unwrap();
421 let supported_sasl_mechanisms = entry.attrs.get("supportedSASLMechanisms").unwrap();
422 let config_context = entry.attrs.get("configContext").unwrap().first().unwrap();
423 let naming_contexts = entry.attrs.get("namingContexts").unwrap();
424 let subschema_subentry = entry
425 .attrs
426 .get("subschemaSubentry")
427 .unwrap()
428 .first()
429 .unwrap();
430 return Ok(Some(RootDSE {
431 supported_ldap_version: supported_ldap_version.to_string(),
432 supported_controls: supported_controls
433 .iter()
434 .map(|x| x.clone().try_into())
435 .collect::<Result<_, _>>()
436 .map_err(OIDError)?,
437 supported_extensions: supported_extensions
438 .iter()
439 .map(|x| x.clone().try_into())
440 .collect::<Result<_, _>>()
441 .map_err(OIDError)?,
442 supported_features: supported_features
443 .iter()
444 .map(|x| x.clone().try_into())
445 .collect::<Result<_, _>>()
446 .map_err(OIDError)?,
447 supported_sasl_mechanisms: supported_sasl_mechanisms.to_vec(),
448 config_context: config_context.to_string(),
449 naming_contexts: naming_contexts.to_vec(),
450 subschema_subentry: subschema_subentry.to_string(),
451 }));
452 }
453 Ok(None)
454}
455
456#[derive(Debug, Error)]
458pub enum LdapSchemaError {
459 #[error("Ldap operation error: {0}")]
461 LdapOperationError(#[from] LdapOperationError),
462 #[error("chumsky parser error: {0}")]
464 ChumskyError(#[from] ChumskyError),
465}
466
467#[instrument(skip(ldap))]
471pub async fn query_ldap_schema(ldap: &mut Ldap) -> Result<Option<LDAPSchema>, LdapSchemaError> {
472 if let Some(root_dse) = query_root_dse(ldap).await? {
473 let mut it = ldap_search(
474 ldap,
475 &root_dse.subschema_subentry,
476 Scope::Base,
477 "(objectClass=*)",
478 vec![
479 "ldapSyntaxes",
480 "matchingRules",
481 "matchingRuleUse",
482 "attributeTypes",
483 "objectClasses",
484 ],
485 )
486 .await?;
487
488 if let Some(entry) = it.next() {
489 let ldap_syntaxes = entry
490 .attrs
491 .get("ldapSyntaxes")
492 .unwrap()
493 .iter()
494 .map(|x| match ldap_syntax_parser().parse_recovery(x.as_str()) {
495 (Some(ldap_syntax), _) => Ok(ldap_syntax),
496 (_, errs) => Err(ChumskyError {
497 description: "ldap syntax".to_string(),
498 source: x.to_string(),
499 errors: errs,
500 }),
501 })
502 .collect::<Result<Vec<LDAPSyntax>, ChumskyError>>()?;
503 let matching_rules = entry
504 .attrs
505 .get("matchingRules")
506 .unwrap()
507 .iter()
508 .map(
509 |x| match matching_rule_parser().parse_recovery(x.as_str()) {
510 (Some(matching_rule), _) => Ok(matching_rule),
511 (_, errs) => Err(ChumskyError {
512 description: "matching rule".to_string(),
513 source: x.to_string(),
514 errors: errs,
515 }),
516 },
517 )
518 .collect::<Result<Vec<MatchingRule>, ChumskyError>>()?;
519 let matching_rule_use = entry
520 .attrs
521 .get("matchingRuleUse")
522 .unwrap()
523 .iter()
524 .map(
525 |x| match matching_rule_use_parser().parse_recovery(x.as_str()) {
526 (Some(matching_rule_use), _) => Ok(matching_rule_use),
527 (_, errs) => Err(ChumskyError {
528 description: "matching rule use".to_string(),
529 source: x.to_string(),
530 errors: errs,
531 }),
532 },
533 )
534 .collect::<Result<Vec<MatchingRuleUse>, ChumskyError>>()?;
535 let attribute_types = entry
536 .attrs
537 .get("attributeTypes")
538 .unwrap()
539 .iter()
540 .map(
541 |x| match attribute_type_parser().parse_recovery(x.as_str()) {
542 (Some(attribute_type), _) => Ok(attribute_type),
543 (_, errs) => Err(ChumskyError {
544 description: "attribute type".to_string(),
545 source: x.to_string(),
546 errors: errs,
547 }),
548 },
549 )
550 .collect::<Result<Vec<AttributeType>, ChumskyError>>()?;
551 let object_classes = entry
552 .attrs
553 .get("objectClasses")
554 .unwrap()
555 .iter()
556 .map(|x| match object_class_parser().parse_recovery(x.as_str()) {
557 (Some(object_class), _) => Ok(object_class),
558 (_, errs) => Err(ChumskyError {
559 description: "object class".to_string(),
560 source: x.to_string(),
561 errors: errs,
562 }),
563 })
564 .collect::<Result<Vec<ObjectClass>, ChumskyError>>()?;
565 return Ok(Some(LDAPSchema {
566 ldap_syntaxes,
567 matching_rules,
568 matching_rule_use,
569 attribute_types,
570 object_classes,
571 }));
572 }
573 }
574 Ok(None)
575}
576
577pub fn success_or_noop_success(
579 ldap_result: ldap3::LdapResult,
580) -> ldap3::result::Result<ldap3::LdapResult> {
581 if ldap_result.rc == 0 || ldap_result.rc == 16654 {
583 Ok(ldap_result)
584 } else {
585 Err(ldap3::LdapError::from(ldap_result))
586 }
587}
588
589#[instrument(skip(ldap))]
591pub async fn delete_recursive(
592 ldap: &mut Ldap,
593 dn: &str,
594 controls: Vec<ldap3::controls::RawControl>,
595) -> Result<(), LdapOperationError> {
596 tracing::debug!("Deleting {} recursively", dn);
597 let it = ldap_search(
598 ldap,
599 dn,
600 Scope::Subtree,
601 "(objectClass=*)",
602 Vec::<String>::new(),
603 )
604 .await?;
605 let mut entries = vec![];
606 for entry in it {
607 tracing::debug!("Found child entry to delete {}", entry.dn);
608 entries.push(entry.dn);
609 }
610 entries.sort_by_key(|b| std::cmp::Reverse(b.len()));
611 for dn in entries {
612 tracing::debug!("Deleting child entry {}", dn);
613 success_or_noop_success(ldap.with_controls(controls.to_owned()).delete(&dn).await?)?;
614 }
615 Ok(())
616}
617
618pub fn mods_as_bin_mods<'a, T>(mods: T) -> Vec<ldap3::Mod<Vec<u8>>>
621where
622 T: IntoIterator<Item = &'a ldap3::Mod<String>>,
623{
624 let mut result: Vec<ldap3::Mod<Vec<u8>>> = vec![];
625 for m in mods {
626 match m {
627 ldap3::Mod::Add(k, v) => {
628 result.push(ldap3::Mod::Add(
629 k.as_bytes().to_vec(),
630 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
631 ));
632 }
633 ldap3::Mod::Delete(k, v) => {
634 result.push(ldap3::Mod::Delete(
635 k.as_bytes().to_vec(),
636 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
637 ));
638 }
639 ldap3::Mod::Replace(k, v) => {
640 result.push(ldap3::Mod::Replace(
641 k.as_bytes().to_vec(),
642 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
643 ));
644 }
645 ldap3::Mod::Increment(k, v) => {
646 result.push(ldap3::Mod::Increment(
647 k.as_bytes().to_vec(),
648 v.as_bytes().to_vec(),
649 ));
650 }
651 }
652 }
653 result
654}
655
656#[instrument(skip(ldap, ldap_operations))]
662pub async fn apply_ldap_operations(
663 ldap: &mut Ldap,
664 ldap_base_dn: &str,
665 ldap_operations: &[LDAPOperation],
666 controls: Vec<ldap3::controls::RawControl>,
667) -> Result<(), LdapOperationError> {
668 tracing::debug!(
669 "The following operations use the LDAP controls: {:#?}",
670 controls
671 );
672 for op in ldap_operations {
673 match op {
674 LDAPOperation::Add(LDAPEntry {
675 dn,
676 attrs,
677 bin_attrs,
678 }) => {
679 let full_dn = format!("{},{}", dn, ldap_base_dn);
680 tracing::debug!(
681 "Adding LDAP entry at {} with attributes\n{:#?}\nand binary attributes\n{:#?}",
682 &full_dn,
683 attrs,
684 bin_attrs
685 );
686 let mut combined_attrs: Vec<(Vec<u8>, HashSet<Vec<u8>>)> = bin_attrs
689 .iter()
690 .map(|(k, v)| {
691 (
692 k.to_owned().as_bytes().to_vec(),
693 v.iter().map(|s| s.to_owned()).collect::<HashSet<Vec<u8>>>(),
694 )
695 })
696 .collect();
697 combined_attrs.extend(attrs.iter().map(|(k, v)| {
698 (
699 k.to_owned().as_bytes().to_vec(),
700 v.iter()
701 .map(|s| s.as_bytes().to_vec())
702 .collect::<HashSet<Vec<u8>>>(),
703 )
704 }));
705 ldap.with_controls(controls.to_owned())
706 .add(&full_dn, combined_attrs)
707 .await?
708 .success()?;
709 }
710 LDAPOperation::Delete { dn } => {
711 let full_dn = format!("{},{}", dn, ldap_base_dn);
712 tracing::debug!("Deleting LDAP entry at {}", &full_dn);
713 delete_recursive(ldap, &full_dn, controls.to_owned()).await?;
714 }
715 LDAPOperation::Modify { dn, mods, bin_mods } => {
716 let full_dn = format!("{},{}", dn, ldap_base_dn);
717 tracing::debug!("Modifying LDAP entry at {} with modifications\n{:#?}\nand binary modifications\n{:#?}", &full_dn, mods, bin_mods);
718 let mut combined_mods = bin_mods.to_owned();
719 combined_mods.extend(mods_as_bin_mods(mods));
720 ldap.with_controls(controls.to_owned())
721 .modify(&full_dn, combined_mods.to_vec())
722 .await?
723 .success()?;
724 }
725 }
726 }
727
728 Ok(())
729}
730
731#[instrument(skip(ldap, entries))]
734pub async fn search_entries(
735 ldap: &mut Ldap,
736 base_dn: &str,
737 search_base: &str,
738 scope: ldap3::Scope,
739 filter: &str,
740 attrs: &[String],
741 entries: &mut HashMap<String, LDAPEntry>,
742) -> Result<(), LdapOperationError> {
743 let it = ldap_search(
744 ldap,
745 &format!("{},{}", search_base, base_dn),
746 scope,
747 filter,
748 attrs.to_owned(),
749 )
750 .await?;
751 for entry in it {
752 tracing::debug!("Found entry {}", entry.dn);
753 if let Some(s) = entry.dn.strip_suffix(&format!(",{}", &base_dn)) {
754 entries.insert(
755 s.to_string(),
756 LDAPEntry {
757 dn: s.to_string(),
758 attrs: entry.attrs,
759 bin_attrs: entry.bin_attrs,
760 },
761 );
762 } else {
763 tracing::error!(
764 "Failed to remove base dn {} from entry DN {}",
765 base_dn,
766 entry.dn
767 );
768 }
769 }
770 Ok(())
771}
772
773#[instrument(skip(
776 source_entry,
777 source_ldap_schema,
778 source_base_dn,
779 destination_entry,
780 destination_base_dn,
781 ignore_object_classes,
782))]
783pub fn mod_value(
784 attr_name: &str,
785 source_entry: &LDAPEntry,
786 source_ldap_schema: &LDAPSchema,
787 source_base_dn: &str,
788 destination_entry: Option<&LDAPEntry>,
789 destination_base_dn: &str,
790 ignore_object_classes: &[String],
791) -> Option<ldap3::Mod<String>> {
792 lazy_static! {
793 static ref DN_SYNTAX_OID: OIDWithLength = OIDWithLength {
794 oid: ObjectIdentifier::try_from("1.3.6.1.4.1.1466.115.121.1.12").unwrap(),
795 length: None
796 };
797 }
798 if let Some(values) = source_entry.attrs.get(attr_name) {
799 let mut replacement_values = HashSet::from_iter(values.iter().cloned());
800 if attr_name == "objectClass" {
801 for io in ignore_object_classes {
802 replacement_values.remove(io);
803 }
804 }
805 let attr_type_syntax =
806 source_ldap_schema.find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
807 tracing::trace!(
808 "Attribute type syntax for altered attribute {}: {:#?}",
809 attr_name,
810 attr_type_syntax
811 );
812 if let Some(syntax) = attr_type_syntax {
813 if DN_SYNTAX_OID.eq(syntax) {
814 tracing::trace!(
815 "Replacing base DN {} with base DN {}",
816 source_base_dn,
817 destination_base_dn
818 );
819 replacement_values = replacement_values
820 .into_iter()
821 .map(|s| s.replace(source_base_dn, destination_base_dn))
822 .collect();
823 }
824 }
825 if let Some(destination_entry) = destination_entry {
826 if let Some(destination_values) = destination_entry.attrs.get(attr_name) {
827 let mut replacement_values_sorted: Vec<String> =
828 replacement_values.iter().cloned().collect();
829 replacement_values_sorted.sort();
830 let mut destination_values: Vec<String> = destination_values.to_vec();
831 destination_values.sort();
832 tracing::trace!("Checking if replacement values and destination values are identical (case sensitive):\n{:#?}\n{:#?}", destination_values, replacement_values_sorted);
833 if replacement_values_sorted == destination_values {
834 tracing::trace!("Skipping attribute {} because replacement values and destination values are identical (case sensitive)", attr_name);
835 return None;
836 }
837 let attr_type_equality = source_ldap_schema
838 .find_attribute_type_property(attr_name, |at| at.equality.as_ref());
839 tracing::trace!(
840 "Attribute type equality for altered attribute {}: {:#?}",
841 attr_name,
842 attr_type_equality
843 );
844 if let Some(equality) = &attr_type_equality {
845 if equality.describes_case_insensitive_match() {
846 let mut lower_destination_values: Vec<String> = destination_values
847 .iter()
848 .map(|s| s.to_lowercase())
849 .collect();
850 lower_destination_values.sort();
851 let mut lower_replacement_values: Vec<String> = replacement_values
852 .iter()
853 .map(|s| s.to_lowercase())
854 .collect();
855 lower_replacement_values.sort();
856 tracing::trace!("Checking if replacement values and destination values are identical (case insensitive):\n{:#?}\n{:#?}", lower_destination_values, lower_replacement_values);
857 if lower_destination_values == lower_replacement_values {
858 tracing::trace!("Skipping attribute {} because replacement values and destination values are identical (case insensitive)", attr_name);
859 return None;
860 }
861 }
862 }
863 }
864 }
865 Some(ldap3::Mod::Replace(
866 attr_name.to_string(),
867 replacement_values,
868 ))
869 } else {
870 Some(ldap3::Mod::Delete(attr_name.to_string(), HashSet::new()))
871 }
872}
873
874#[allow(clippy::too_many_arguments)]
878#[instrument(skip(source_ldap_schema))]
879pub fn diff_entries(
880 source_entries: &HashMap<String, LDAPEntry>,
881 destination_entries: &HashMap<String, LDAPEntry>,
882 source_base_dn: &str,
883 destination_base_dn: &str,
884 ignore_object_classes: &[String],
885 ignore_attributes: &[String],
886 source_ldap_schema: &LDAPSchema,
887 add: bool,
888 update: bool,
889 delete: bool,
890) -> Vec<LDAPOperation> {
891 lazy_static! {
892 static ref DN_SYNTAX_OID: OIDWithLength = OIDWithLength {
893 oid: ObjectIdentifier::try_from("1.3.6.1.4.1.1466.115.121.1.12").unwrap(),
894 length: None
895 };
896 }
897 let diff = Diff::diff(source_entries, destination_entries);
898 tracing::trace!("Diff:\n{:#?}", diff);
899 let mut ldap_operations: Vec<LDAPOperation> = vec![];
900 for (altered_dn, change) in diff.altered {
901 tracing::trace!("Processing altered DN {}", altered_dn);
902 let source_entry: Option<&LDAPEntry> = source_entries.get(&altered_dn);
903 let destination_entry: Option<&LDAPEntry> = destination_entries.get(&altered_dn);
904 if let Some(source_entry) = source_entry {
905 let mut ldap_mods: Vec<ldap3::Mod<String>> = vec![];
906 let mut ldap_bin_mods: Vec<ldap3::Mod<Vec<u8>>> = vec![];
907 for (attr_name, attr_value_changes) in &change.attrs.altered {
908 if ignore_attributes.contains(attr_name) {
909 continue;
910 }
911 for attr_value_change in &attr_value_changes.0 {
912 match attr_value_change {
913 VecDiffType::Removed { .. }
914 | VecDiffType::Inserted { .. }
915 | VecDiffType::Altered { .. } => {
916 let m = mod_value(
917 attr_name,
918 source_entry,
919 source_ldap_schema,
920 source_base_dn,
921 destination_entry,
922 destination_base_dn,
923 ignore_object_classes,
924 );
925 if let Some(m) = m {
926 if !ldap_mods.contains(&m) {
927 ldap_mods.push(m);
928 }
929 }
930 }
931 }
932 }
933 }
934 for attr_name in &change.attrs.removed {
935 if ignore_attributes.contains(attr_name) {
936 continue;
937 }
938 let mut replacement_values =
939 HashSet::from_iter(source_entry.attrs[attr_name].iter().cloned());
940 if attr_name == "objectClass" {
941 for io in ignore_object_classes {
942 replacement_values.remove(io);
943 }
944 }
945 let attr_type_syntax = source_ldap_schema
946 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
947 tracing::trace!(
948 "Attribute type syntax for deleted attribute {}: {:#?}",
949 attr_name,
950 attr_type_syntax
951 );
952 if let Some(syntax) = attr_type_syntax {
953 if DN_SYNTAX_OID.eq(syntax) {
954 tracing::trace!(
955 "Replacing base DN {} with base DN {}",
956 source_base_dn,
957 destination_base_dn
958 );
959 replacement_values = replacement_values
960 .into_iter()
961 .map(|s| s.replace(source_base_dn, destination_base_dn))
962 .collect();
963 }
964 }
965 ldap_mods.push(ldap3::Mod::Add(attr_name.to_string(), replacement_values));
966 }
967 for (attr_name, attr_value_changes) in &change.bin_attrs.altered {
968 if ignore_attributes.contains(attr_name) {
969 continue;
970 }
971 for attr_value_change in &attr_value_changes.0 {
972 match attr_value_change {
973 VecDiffType::Removed { .. }
974 | VecDiffType::Inserted { .. }
975 | VecDiffType::Altered { .. } => {
976 if let Some(values) = source_entry.bin_attrs.get(attr_name) {
977 let replace_mod = ldap3::Mod::Replace(
978 attr_name.as_bytes().to_vec(),
979 HashSet::from_iter(values.iter().cloned()),
980 );
981 if !ldap_bin_mods.contains(&replace_mod) {
982 ldap_bin_mods.push(replace_mod)
983 }
984 } else {
985 ldap_bin_mods.push(ldap3::Mod::Delete(
986 attr_name.as_bytes().to_vec(),
987 HashSet::new(),
988 ));
989 }
990 }
991 }
992 }
993 }
994 for attr_name in &change.bin_attrs.removed {
995 if ignore_attributes.contains(attr_name) {
996 continue;
997 }
998 ldap_bin_mods.push(ldap3::Mod::Add(
999 attr_name.as_bytes().to_vec(),
1000 HashSet::from_iter(source_entry.bin_attrs[attr_name].iter().cloned()),
1001 ));
1002 }
1003 if update && !(ldap_mods.is_empty() && ldap_bin_mods.is_empty()) {
1004 ldap_operations.push(LDAPOperation::Modify {
1005 dn: source_entry.dn.clone(),
1006 mods: ldap_mods,
1007 bin_mods: ldap_bin_mods,
1008 });
1009 }
1010 } else if delete {
1011 ldap_operations.push(LDAPOperation::Delete {
1012 dn: altered_dn.clone(),
1013 });
1014 }
1015 }
1016 for removed_dn in diff.removed {
1017 if add {
1018 let mut new_entry = source_entries[&removed_dn].clone();
1019 for ia in ignore_attributes {
1020 new_entry.attrs.remove(ia);
1021 new_entry.bin_attrs.remove(ia);
1022 }
1023 if let Some((k, v)) = new_entry.attrs.remove_entry("objectClass") {
1024 let ioc = &ignore_object_classes;
1025 let new_v = v.into_iter().filter(|x| !ioc.contains(x)).collect();
1026 new_entry.attrs.insert(k, new_v);
1027 }
1028 for (attr_name, attr_values) in new_entry.attrs.iter_mut() {
1029 let attr_type_syntax = source_ldap_schema
1030 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
1031 tracing::trace!(
1032 "Attribute type syntax for attribute {} in deleted entry {}: {:#?}",
1033 attr_name,
1034 removed_dn,
1035 attr_type_syntax
1036 );
1037 if let Some(syntax) = attr_type_syntax {
1038 if DN_SYNTAX_OID.eq(syntax) {
1039 tracing::trace!(
1040 "Replacing base DN {} with base DN {}",
1041 source_base_dn,
1042 destination_base_dn
1043 );
1044 for s in attr_values.iter_mut() {
1045 *s = s.replace(source_base_dn, destination_base_dn);
1046 }
1047 }
1048 }
1049 }
1050 ldap_operations.push(LDAPOperation::Add(new_entry));
1051 }
1052 }
1053
1054 ldap_operations.sort_by(|a, b| {
1055 a.operation_apply_cmp(b)
1056 .unwrap_or(std::cmp::Ordering::Equal)
1057 });
1058
1059 ldap_operations
1060}