1#![doc = include_str!("../README.md")]
2
3use diff::{Diff, VecDiffType};
4
5use chumsky::Parser as _;
6use ldap_types::basic::{ChumskyError, LDAPEntry, LDAPOperation, OIDWithLength, RootDSE};
7use ldap_types::schema::{
8 AttributeType, LDAPSchema, LDAPSyntax, MatchingRule, MatchingRuleUse, ObjectClass,
9 attribute_type_parser, ldap_syntax_parser, matching_rule_parser, matching_rule_use_parser,
10 object_class_parser,
11};
12
13use ldap3::exop::{WhoAmI, WhoAmIResp};
14use ldap3::result::SearchResult;
15use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
16use native_tls::{Certificate, Identity, TlsConnector};
17use oid::ObjectIdentifier;
18
19use std::sync::LazyLock;
20
21pub static DN_SYNTAX_OID: LazyLock<Result<OIDWithLength, oid::ObjectIdentifierError>> =
23 LazyLock::new(|| {
24 Ok(OIDWithLength {
25 oid: ObjectIdentifier::try_from("1.3.6.1.4.1.1466.115.121.1.12")?,
26 length: None,
27 })
28 });
29
30use std::collections::{HashMap, HashSet};
31use std::fmt::Debug;
32use std::fmt::Display;
33
34use openssl::pkcs12::Pkcs12;
35use openssl::pkey::PKey;
36use openssl::x509::X509;
37
38use std::fs::File;
39use std::io::Read as _;
40use std::path::Path;
41
42use regex::Regex;
43
44use dirs2::home_dir;
45
46use tracing::instrument;
47
48use derive_builder::Builder;
49
50use serde::Deserialize;
51
52use thiserror::Error;
53
54#[must_use]
64pub fn noop_control() -> ldap3::controls::RawControl {
65 ldap3::controls::RawControl {
66 ctype: "1.3.6.1.4.1.4203.666.5.2".to_string(),
67 crit: true,
68 val: None,
69 }
70}
71
72#[derive(Debug, Clone, Error)]
74pub enum ScopeParserError {
75 #[error("Could not parse {0} as an ldap scope")]
77 CouldNotParseAsScope(String),
78}
79
80pub fn parse_scope(src: &str) -> Result<ldap3::Scope, ScopeParserError> {
87 match src {
88 "base" => Ok(ldap3::Scope::Base),
89 "one" => Ok(ldap3::Scope::OneLevel),
90 "sub" => Ok(ldap3::Scope::Subtree),
91 s => Err(ScopeParserError::CouldNotParseAsScope(s.to_string())),
92 }
93}
94
95#[derive(Debug, Clone, Builder, Deserialize)]
98pub struct ConnectParameters {
99 ca_cert_path: std::string::String,
101 client_cert_path: std::string::String,
103 client_key_path: std::string::String,
105 pub url: std::string::String,
107}
108
109#[derive(Debug, Error)]
111pub enum OpenLdapConnectParameterError {
112 #[error("regex error: {0}")]
114 RegexError(#[from] regex::Error),
115 #[error("I/O error: {0}")]
117 IOError(#[from] std::io::Error),
118}
119
120#[instrument(skip(builder))]
127pub fn openldap_connect_parameters(
128 builder: &mut ConnectParametersBuilder,
129) -> Result<&mut ConnectParametersBuilder, OpenLdapConnectParameterError> {
130 let ldap_rc_content;
131 let ldap_conf_content;
132 if let Some(d) = home_dir() {
133 let ldap_rc_filename = d.join(".ldaprc");
134 if ldap_rc_filename.exists() {
135 tracing::debug!("Using .ldaprc at {:?}", ldap_rc_filename);
136 ldap_rc_content = fs_err::read_to_string(ldap_rc_filename)?;
137
138 let ca_cert_re = Regex::new(r"^TLS_CACERT *(.*)$")?;
139 let client_cert_re = Regex::new(r"^TLS_CERT *(.*)$")?;
140 let client_key_re = Regex::new(r"^TLS_KEY *(.*)$")?;
141 for line in ldap_rc_content.lines() {
142 if let Some(ca_cert_path) = ca_cert_re
143 .captures(line)
144 .and_then(|caps| caps.get(1))
145 .map(|m| m.as_str())
146 {
147 tracing::debug!("Extracted .ldaprc TLS_CACERT value {}", ca_cert_path);
148 builder.ca_cert_path(ca_cert_path.to_string());
149 }
150
151 if let Some(client_cert_path) = client_cert_re
152 .captures(line)
153 .and_then(|caps| caps.get(1))
154 .map(|m| m.as_str())
155 {
156 tracing::debug!("Extracted .ldaprc TLS_CERT value {}", client_cert_path);
157 builder.client_cert_path(client_cert_path.to_string());
158 }
159
160 if let Some(client_key_path) = client_key_re
161 .captures(line)
162 .and_then(|caps| caps.get(1))
163 .map(|m| m.as_str())
164 {
165 tracing::debug!("Extracted .ldaprc TLS_KEY value {}", client_key_path);
166 builder.client_key_path(client_key_path.to_string());
167 }
168 }
169 }
170
171 let mut ldap_conf_filename = Path::new("/etc/ldap/ldap.conf");
172 if !ldap_conf_filename.exists() {
173 ldap_conf_filename = Path::new("/etc/openldap/ldap.conf");
174 }
175 if ldap_conf_filename.exists() {
176 tracing::debug!("Using ldap.conf at {:?}", ldap_conf_filename);
177 ldap_conf_content = fs_err::read_to_string(ldap_conf_filename)?;
178
179 let uri_re = Regex::new(r"^URI *(.*)$")?;
180 for line in ldap_conf_content.lines() {
181 if let Some(url) = uri_re
182 .captures(line)
183 .and_then(|caps| caps.get(1))
184 .map(|m| m.as_str())
185 {
186 tracing::debug!("Extracted ldap.conf URI value {}", url);
187 builder.url(url.to_string());
188 }
189 }
190 }
191 }
192 Ok(builder)
193}
194
195#[instrument(skip(builder))]
199pub fn default_connect_parameters(
200 builder: &mut ConnectParametersBuilder,
201) -> &mut ConnectParametersBuilder {
202 if builder.ca_cert_path.is_none() {
203 builder.ca_cert_path("ca.crt".to_string());
204 }
205 if builder.client_cert_path.is_none() {
206 builder.client_cert_path("client.crt".to_string());
207 }
208 if builder.client_key_path.is_none() {
209 builder.client_key_path("client.key".to_string());
210 }
211 builder
212}
213
214#[derive(Debug, Error)]
216pub enum TomlConfigError {
217 #[error("I/O error: {0}")]
219 IOError(#[from] std::io::Error),
220 #[error("Toml deserialization error: {0}")]
222 TomlError(#[from] toml::de::Error),
223}
224
225#[instrument]
231pub fn toml_connect_parameters(
232 filename: std::path::PathBuf,
233) -> Result<ConnectParameters, TomlConfigError> {
234 let config = fs_err::read_to_string(filename)?;
235 let result: ConnectParameters = toml::from_str(&config)?;
236
237 Ok(result)
238}
239
240#[derive(Debug, Error)]
242pub enum ConnectError {
243 #[error("Parameters builder error: {0}")]
246 ParametersBuilderError(#[from] ConnectParametersBuilderError),
247 #[error("Error retrieving OpenLDAP connect parameters: {0}")]
249 OpenLdapConnectParameterError(#[from] OpenLdapConnectParameterError),
250 #[error("I/O error: {0}")]
252 IOError(#[from] std::io::Error),
253 #[error("Native TLS error: {0}")]
255 NativeTLSError(#[from] native_tls::Error),
256 #[error("ldap3 Ldap error: {0}")]
258 LdapError(#[from] ldap3::LdapError),
259 #[error("regex error: {0}")]
261 RegexError(#[from] regex::Error),
262 #[error("openssl error: {0}")]
264 OpenSSLError(#[from] openssl::error::ErrorStack),
265}
266
267#[instrument]
275pub async fn connect() -> Result<(Ldap, std::string::String), ConnectError> {
276 let mut builder = ConnectParametersBuilder::default();
277 openldap_connect_parameters(&mut builder)?;
278 match builder.build() {
279 Ok(result) => connect_with_parameters(result).await,
280 Err(err_msg) => {
281 tracing::error!(
282 "Building of ConnectParameters based on OpenLDAP config files failed: {}",
283 err_msg
284 );
285 let builder = default_connect_parameters(&mut builder);
286 match builder.build() {
287 Ok(result) => connect_with_parameters(result).await,
288 Err(err) => {
289 tracing::error!(
290 "Building of ConnectParameters based on OpenLDAP config files and substituting default values for missing values failed: {}",
291 err
292 );
293 Err(ConnectError::ParametersBuilderError(err))
294 }
295 }
296 }
297 }
298}
299
300#[instrument]
307pub async fn connect_with_parameters(
308 connect_parameters: ConnectParameters,
309) -> Result<(Ldap, std::string::String), ConnectError> {
310 let mut client_cert_contents = Vec::new();
311 {
312 let mut file = File::open(connect_parameters.client_cert_path)?;
313 file.read_to_end(&mut client_cert_contents)?;
314 }
315 let client_cert = X509::from_pem(&client_cert_contents)?;
316 let mut client_key_contents = Vec::new();
317 {
318 let mut file = File::open(connect_parameters.client_key_path)?;
319 file.read_to_end(&mut client_key_contents)?;
320 }
321 let client_key = PKey::private_key_from_pem(&client_key_contents)?;
322 let p12_password = "client";
323 let p12 = Pkcs12::builder()
324 .name("client")
325 .pkey(&client_key)
326 .cert(&client_cert)
327 .build2(p12_password)?;
328 let p12_contents = p12.to_der()?;
329 let mut ca_cert_contents = Vec::new();
330 {
331 let mut file = File::open(connect_parameters.ca_cert_path)?;
332 file.read_to_end(&mut ca_cert_contents)?;
333 }
334 let identity = Identity::from_pkcs12(&p12_contents, p12_password)?;
335 let ca_certificate = Certificate::from_pem(&ca_cert_contents)?;
336 let connector = TlsConnector::builder()
337 .identity(identity)
338 .add_root_certificate(ca_certificate)
339 .build()?;
340 let ldap_settings = LdapConnSettings::new().set_connector(connector);
341 let (ldap_conn_async, mut ldap) =
342 LdapConnAsync::with_settings(ldap_settings, &connect_parameters.url.clone()).await?;
343 ldap3::drive!(ldap_conn_async);
344 ldap.sasl_external_bind().await?;
345 let (exop, _res) = ldap.extended(WhoAmI).await?.success()?;
346 let who_am_i: WhoAmIResp = exop.parse();
347 let re = Regex::new(r"^.*,ou=[a-z]+,")?;
348 let base_dn = re.replace_all(&who_am_i.authzid, "").to_string();
349 Ok((ldap, base_dn))
350}
351
352#[derive(Debug, Error)]
354pub enum LdapOperationError {
355 #[error("ldap3 Ldap error: {0}")]
357 LdapError(#[from] ldap3::LdapError),
358 #[error("OID error: {0}")]
360 OIDError(#[from] OIDError),
361 #[error("Missing expected attribute: {0}")]
363 MissingAttribute(String),
364}
365
366pub async fn ldap_search<'a, S: AsRef<str> + Clone + Display + Debug + Send + Sync>(
373 ldap: &mut Ldap,
374 base: &str,
375 scope: Scope,
376 filter: &str,
377 attrs: Vec<S>,
378) -> Result<Box<dyn Iterator<Item = SearchEntry> + 'a>, LdapOperationError> {
379 let search_result = ldap.search(base, scope, filter, attrs.clone()).await?;
380 let SearchResult(_rs, res) = &search_result;
381 if res.rc != 0 {
382 tracing::debug!(
383 "Non-zero return code {} in LDAP query\n base: {}\n scope: {:?}\n filter: {}\n attrs: {:#?}",
384 res.rc,
385 base,
386 scope,
387 filter,
388 attrs
389 );
390 tracing::debug!(
391 "ldapsearch -Q -LLL -o ldif-wrap=no -b '{}' -s {} '{}' {}",
392 base,
393 format!("{scope:?}").to_lowercase(),
394 filter,
395 itertools::join(attrs.iter(), " ")
396 );
397 }
398 let (rs, _res) = search_result.success()?;
399 Ok(Box::new(rs.into_iter().map(SearchEntry::construct)))
400}
401
402#[derive(Debug)]
405pub struct OIDError(oid::ObjectIdentifierError);
406
407impl Display for OIDError {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 write!(f, "Error parsing OID: {:?}", self.0)
410 }
411}
412
413impl std::error::Error for OIDError {
414 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
415 None
416 }
417}
418
419#[instrument(skip(ldap))]
426pub async fn query_root_dse(ldap: &mut Ldap) -> Result<Option<RootDSE>, LdapOperationError> {
427 let mut it = ldap_search(
428 ldap,
429 "",
430 Scope::Base,
431 "(objectClass=*)",
432 vec![
433 "supportedLDAPVersion",
434 "supportedControl",
435 "supportedExtension",
436 "supportedFeatures",
437 "supportedSASLMechanisms",
438 "configContext",
439 "namingContexts",
440 "subschemaSubentry",
441 ],
442 )
443 .await?;
444 if let Some(entry) = it.next() {
445 let supported_ldap_version = entry
446 .attrs
447 .get("supportedLDAPVersion")
448 .ok_or(LdapOperationError::MissingAttribute(
449 "supportedLDAPVersion".to_string(),
450 ))?
451 .first()
452 .ok_or(LdapOperationError::MissingAttribute(
453 "supportedLDAPVersion".to_string(),
454 ))?;
455 let supported_controls =
456 entry
457 .attrs
458 .get("supportedControl")
459 .ok_or(LdapOperationError::MissingAttribute(
460 "supportedControl".to_string(),
461 ))?;
462 let supported_extensions =
463 entry
464 .attrs
465 .get("supportedExtension")
466 .ok_or(LdapOperationError::MissingAttribute(
467 "supportedExtension".to_string(),
468 ))?;
469 let supported_features =
470 entry
471 .attrs
472 .get("supportedFeatures")
473 .ok_or(LdapOperationError::MissingAttribute(
474 "supportedFeatures".to_string(),
475 ))?;
476 let supported_sasl_mechanisms = entry.attrs.get("supportedSASLMechanisms").ok_or(
477 LdapOperationError::MissingAttribute("supportedSASLMechanisms".to_string()),
478 )?;
479 let config_context = entry
480 .attrs
481 .get("configContext")
482 .ok_or(LdapOperationError::MissingAttribute(
483 "configContext".to_string(),
484 ))?
485 .first()
486 .ok_or(LdapOperationError::MissingAttribute(
487 "configContext".to_string(),
488 ))?;
489 let naming_contexts =
490 entry
491 .attrs
492 .get("namingContexts")
493 .ok_or(LdapOperationError::MissingAttribute(
494 "namingContexts".to_string(),
495 ))?;
496 let subschema_subentry = entry
497 .attrs
498 .get("subschemaSubentry")
499 .ok_or(LdapOperationError::MissingAttribute(
500 "subschemaSubentry".to_string(),
501 ))?
502 .first()
503 .ok_or(LdapOperationError::MissingAttribute(
504 "subschemaSubentry".to_string(),
505 ))?;
506 return Ok(Some(RootDSE {
507 supported_ldap_version: supported_ldap_version.to_string(),
508 supported_controls: supported_controls
509 .iter()
510 .map(|x| x.clone().try_into())
511 .collect::<Result<_, _>>()
512 .map_err(OIDError)?,
513 supported_extensions: supported_extensions
514 .iter()
515 .map(|x| x.clone().try_into())
516 .collect::<Result<_, _>>()
517 .map_err(OIDError)?,
518 supported_features: supported_features
519 .iter()
520 .map(|x| x.clone().try_into())
521 .collect::<Result<_, _>>()
522 .map_err(OIDError)?,
523 supported_sasl_mechanisms: supported_sasl_mechanisms.to_vec(),
524 config_context: config_context.to_string(),
525 naming_contexts: naming_contexts.to_vec(),
526 subschema_subentry: subschema_subentry.to_string(),
527 }));
528 }
529 Ok(None)
530}
531
532#[derive(Debug, Error)]
534pub enum LdapSchemaError {
535 #[error("Ldap operation error: {0}")]
537 LdapOperationError(#[from] LdapOperationError),
538 #[error("chumsky parser error: {0}")]
540 ChumskyError(#[from] ChumskyError<chumsky::error::Rich<'static, char>>),
541}
542
543#[instrument(skip(ldap))]
551pub async fn query_ldap_schema(ldap: &mut Ldap) -> Result<Option<LDAPSchema>, LdapSchemaError> {
552 if let Some(root_dse) = query_root_dse(ldap).await? {
553 let mut it = ldap_search(
554 ldap,
555 &root_dse.subschema_subentry,
556 Scope::Base,
557 "(objectClass=*)",
558 vec![
559 "ldapSyntaxes",
560 "matchingRules",
561 "matchingRuleUse",
562 "attributeTypes",
563 "objectClasses",
564 ],
565 )
566 .await?;
567
568 if let Some(entry) = it.next() {
569 let ldap_syntaxes = entry
570 .attrs
571 .get("ldapSyntaxes")
572 .ok_or(LdapOperationError::MissingAttribute(
573 "ldapSyntaxes".to_string(),
574 ))?
575 .iter()
576 .map(|x| match ldap_syntax_parser().parse(x.as_str()).into_output_errors() {
577 (Some(ldap_syntax), _) => Ok(ldap_syntax),
578 (_, errs) => Err(ChumskyError {
579 description: "ldap syntax".to_string(),
580 source: x.to_string(),
581 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
582 }),
583 })
584 .collect::<Result<Vec<LDAPSyntax>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
585 let matching_rules = entry
586 .attrs
587 .get("matchingRules")
588 .ok_or(LdapOperationError::MissingAttribute(
589 "matchingRules".to_string(),
590 ))?
591 .iter()
592 .map(
593 |x| match matching_rule_parser().parse(x.as_str()).into_output_errors() {
594 (Some(matching_rule), _) => Ok(matching_rule),
595 (_, errs) => Err(ChumskyError {
596 description: "matching rule".to_string(),
597 source: x.to_string(),
598 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
599 }),
600 },
601 )
602 .collect::<Result<Vec<MatchingRule>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
603 let matching_rule_use =
604 entry
605 .attrs
606 .get("matchingRuleUse")
607 .ok_or(LdapOperationError::MissingAttribute(
608 "matchingRuleUse".to_string(),
609 ))?
610 .iter()
611 .map(|x| {
612 match matching_rule_use_parser()
613 .parse(x.as_str())
614 .into_output_errors()
615 {
616 (Some(matching_rule_use), _) => Ok(matching_rule_use),
617 (_, errs) => Err(ChumskyError {
618 description: "matching rule use".to_string(),
619 source: x.to_string(),
620 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
621 }),
622 }
623 })
624 .collect::<Result<
625 Vec<MatchingRuleUse>,
626 ChumskyError<chumsky::error::Rich<'static, char>>,
627 >>()?;
628 let attribute_types = entry
629 .attrs
630 .get("attributeTypes")
631 .ok_or(LdapOperationError::MissingAttribute(
632 "attributeTypes".to_string(),
633 ))?
634 .iter()
635 .map(
636 |x| match attribute_type_parser().parse(x.as_str()).into_output_errors() {
637 (Some(attribute_type), _) => Ok(attribute_type),
638 (_, errs) => Err(ChumskyError {
639 description: "attribute type".to_string(),
640 source: x.to_string(),
641 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
642 }),
643 },
644 )
645 .collect::<Result<Vec<AttributeType>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
646 let object_classes = entry
647 .attrs
648 .get("objectClasses")
649 .ok_or(LdapOperationError::MissingAttribute(
650 "objectClasses".to_string(),
651 ))?
652 .iter()
653 .map(|x| match object_class_parser().parse(x.as_str()).into_output_errors() {
654 (Some(object_class), _) => Ok(object_class),
655 (_, errs) => Err(ChumskyError {
656 description: "object class".to_string(),
657 source: x.to_string(),
658 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
659 }),
660 })
661 .collect::<Result<Vec<ObjectClass>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
662 return Ok(Some(LDAPSchema {
663 ldap_syntaxes,
664 matching_rules,
665 matching_rule_use,
666 attribute_types,
667 object_classes,
668 }));
669 }
670 }
671 Ok(None)
672}
673
674pub fn success_or_noop_success(
680 ldap_result: ldap3::LdapResult,
681) -> ldap3::result::Result<ldap3::LdapResult> {
682 if ldap_result.rc == 0 || ldap_result.rc == 16654 {
684 Ok(ldap_result)
685 } else {
686 Err(ldap3::LdapError::from(ldap_result))
687 }
688}
689
690#[instrument(skip(ldap))]
696pub async fn delete_recursive(
697 ldap: &mut Ldap,
698 dn: &str,
699 controls: Vec<ldap3::controls::RawControl>,
700) -> Result<(), LdapOperationError> {
701 tracing::debug!("Deleting {} recursively", dn);
702 let it = ldap_search(
703 ldap,
704 dn,
705 Scope::Subtree,
706 "(objectClass=*)",
707 Vec::<String>::new(),
708 )
709 .await?;
710 let mut entries = vec![];
711 for entry in it {
712 tracing::debug!("Found child entry to delete {}", entry.dn);
713 entries.push(entry.dn);
714 }
715 entries.sort_by_key(|b| std::cmp::Reverse(b.len()));
716 for dn in entries {
717 tracing::debug!("Deleting child entry {}", dn);
718 success_or_noop_success(ldap.with_controls(controls.to_owned()).delete(&dn).await?)?;
719 }
720 Ok(())
721}
722
723pub fn mods_as_bin_mods<'a, T>(mods: T) -> Vec<ldap3::Mod<Vec<u8>>>
726where
727 T: IntoIterator<Item = &'a ldap3::Mod<String>>,
728{
729 let mut result: Vec<ldap3::Mod<Vec<u8>>> = vec![];
730 for m in mods {
731 match m {
732 ldap3::Mod::Add(k, v) => {
733 result.push(ldap3::Mod::Add(
734 k.as_bytes().to_vec(),
735 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
736 ));
737 }
738 ldap3::Mod::Delete(k, v) => {
739 result.push(ldap3::Mod::Delete(
740 k.as_bytes().to_vec(),
741 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
742 ));
743 }
744 ldap3::Mod::Replace(k, v) => {
745 result.push(ldap3::Mod::Replace(
746 k.as_bytes().to_vec(),
747 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
748 ));
749 }
750 ldap3::Mod::Increment(k, v) => {
751 result.push(ldap3::Mod::Increment(
752 k.as_bytes().to_vec(),
753 v.as_bytes().to_vec(),
754 ));
755 }
756 }
757 }
758 result
759}
760
761#[instrument(skip(ldap, ldap_operations))]
771pub async fn apply_ldap_operations(
772 ldap: &mut Ldap,
773 ldap_base_dn: &str,
774 ldap_operations: &[LDAPOperation],
775 controls: Vec<ldap3::controls::RawControl>,
776) -> Result<(), LdapOperationError> {
777 tracing::debug!(
778 "The following operations use the LDAP controls: {:#?}",
779 controls
780 );
781 for op in ldap_operations {
782 match op {
783 LDAPOperation::Add(LDAPEntry {
784 dn,
785 attrs,
786 bin_attrs,
787 }) => {
788 let full_dn = format!("{dn},{ldap_base_dn}");
789 tracing::debug!(
790 "Adding LDAP entry at {} with attributes\n{:#?}\nand binary attributes\n{:#?}",
791 &full_dn,
792 attrs,
793 bin_attrs
794 );
795 let mut combined_attrs: Vec<(Vec<u8>, HashSet<Vec<u8>>)> = bin_attrs
798 .iter()
799 .map(|(k, v)| {
800 (
801 k.to_owned().as_bytes().to_vec(),
802 v.iter().map(|s| s.to_owned()).collect::<HashSet<Vec<u8>>>(),
803 )
804 })
805 .collect();
806 combined_attrs.extend(attrs.iter().map(|(k, v)| {
807 (
808 k.to_owned().as_bytes().to_vec(),
809 v.iter()
810 .map(|s| s.as_bytes().to_vec())
811 .collect::<HashSet<Vec<u8>>>(),
812 )
813 }));
814 ldap.with_controls(controls.to_owned())
815 .add(&full_dn, combined_attrs)
816 .await?
817 .success()?;
818 }
819 LDAPOperation::Delete { dn } => {
820 let full_dn = format!("{dn},{ldap_base_dn}");
821 tracing::debug!("Deleting LDAP entry at {}", &full_dn);
822 delete_recursive(ldap, &full_dn, controls.to_owned()).await?;
823 }
824 LDAPOperation::Modify { dn, mods, bin_mods } => {
825 let full_dn = format!("{dn},{ldap_base_dn}");
826 tracing::debug!(
827 "Modifying LDAP entry at {} with modifications\n{:#?}\nand binary modifications\n{:#?}",
828 &full_dn,
829 mods,
830 bin_mods
831 );
832 let mut combined_mods = bin_mods.to_owned();
833 combined_mods.extend(mods_as_bin_mods(mods));
834 ldap.with_controls(controls.to_owned())
835 .modify(&full_dn, combined_mods.to_vec())
836 .await?
837 .success()?;
838 }
839 }
840 }
841
842 Ok(())
843}
844
845#[instrument(skip(ldap, entries))]
852pub async fn search_entries(
853 ldap: &mut Ldap,
854 base_dn: &str,
855 search_base: &str,
856 scope: ldap3::Scope,
857 filter: &str,
858 attrs: &[String],
859 entries: &mut HashMap<String, LDAPEntry>,
860) -> Result<(), LdapOperationError> {
861 let it = ldap_search(
862 ldap,
863 &format!("{search_base},{base_dn}"),
864 scope,
865 filter,
866 attrs.to_owned(),
867 )
868 .await?;
869 for entry in it {
870 tracing::debug!("Found entry {}", entry.dn);
871 if let Some(s) = entry.dn.strip_suffix(&format!(",{}", &base_dn)) {
872 entries.insert(
873 s.to_string(),
874 LDAPEntry {
875 dn: s.to_string(),
876 attrs: entry.attrs,
877 bin_attrs: entry.bin_attrs,
878 },
879 );
880 } else {
881 tracing::error!(
882 "Failed to remove base dn {} from entry DN {}",
883 base_dn,
884 entry.dn
885 );
886 }
887 }
888 Ok(())
889}
890
891#[instrument(skip(
898 source_entry,
899 source_ldap_schema,
900 source_base_dn,
901 destination_entry,
902 destination_base_dn,
903 ignore_object_classes,
904))]
905pub fn mod_value(
906 attr_name: &str,
907 source_entry: &LDAPEntry,
908 source_ldap_schema: &LDAPSchema,
909 source_base_dn: &str,
910 destination_entry: Option<&LDAPEntry>,
911 destination_base_dn: &str,
912 ignore_object_classes: &[String],
913) -> Result<Option<ldap3::Mod<String>>, LdapOperationError> {
914 if let Some(values) = source_entry.attrs.get(attr_name) {
915 let mut replacement_values = HashSet::from_iter(values.iter().cloned());
916 if attr_name == "objectClass" {
917 for io in ignore_object_classes {
918 replacement_values.remove(io);
919 }
920 }
921 let attr_type_syntax =
922 source_ldap_schema.find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
923 tracing::trace!(
924 "Attribute type syntax for altered attribute {}: {:#?}",
925 attr_name,
926 attr_type_syntax
927 );
928 if let Some(syntax) = attr_type_syntax
929 && (*DN_SYNTAX_OID)
930 .as_ref()
931 .map_err(|e| OIDError(*e))?
932 .eq(syntax)
933 {
934 tracing::trace!(
935 "Replacing base DN {} with base DN {}",
936 source_base_dn,
937 destination_base_dn
938 );
939 replacement_values = replacement_values
940 .into_iter()
941 .map(|s| s.replace(source_base_dn, destination_base_dn))
942 .collect();
943 }
944 if let Some(destination_entry) = destination_entry
945 && let Some(destination_values) = destination_entry.attrs.get(attr_name)
946 {
947 let mut replacement_values_sorted: Vec<String> =
948 replacement_values.iter().cloned().collect();
949 replacement_values_sorted.sort();
950 let mut destination_values: Vec<String> = destination_values.to_vec();
951 destination_values.sort();
952 tracing::trace!(
953 "Checking if replacement values and destination values are identical (case sensitive):\n{:#?}\n{:#?}",
954 destination_values,
955 replacement_values_sorted
956 );
957 if replacement_values_sorted == destination_values {
958 tracing::trace!(
959 "Skipping attribute {} because replacement values and destination values are identical (case sensitive)",
960 attr_name
961 );
962 return Ok(None);
963 }
964 let attr_type_equality = source_ldap_schema
965 .find_attribute_type_property(attr_name, |at| at.equality.as_ref());
966 tracing::trace!(
967 "Attribute type equality for altered attribute {}: {:#?}",
968 attr_name,
969 attr_type_equality
970 );
971 if let Some(equality) = &attr_type_equality
972 && equality.describes_case_insensitive_match()
973 {
974 let mut lower_destination_values: Vec<String> = destination_values
975 .iter()
976 .map(|s| s.to_lowercase())
977 .collect();
978 lower_destination_values.sort();
979 let mut lower_replacement_values: Vec<String> = replacement_values
980 .iter()
981 .map(|s| s.to_lowercase())
982 .collect();
983 lower_replacement_values.sort();
984 tracing::trace!(
985 "Checking if replacement values and destination values are identical (case insensitive):\n{:#?}\n{:#?}",
986 lower_destination_values,
987 lower_replacement_values
988 );
989 if lower_destination_values == lower_replacement_values {
990 tracing::trace!(
991 "Skipping attribute {} because replacement values and destination values are identical (case insensitive)",
992 attr_name
993 );
994 return Ok(None);
995 }
996 }
997 }
998 Ok(Some(ldap3::Mod::Replace(
999 attr_name.to_string(),
1000 replacement_values,
1001 )))
1002 } else {
1003 Ok(Some(ldap3::Mod::Delete(
1004 attr_name.to_string(),
1005 HashSet::new(),
1006 )))
1007 }
1008}
1009
1010#[expect(
1018 clippy::too_many_arguments,
1019 reason = "factoring parameters into objects is not sensible since this is basically standalone without many connections to the rest of the crate"
1020)]
1021#[instrument(skip(source_ldap_schema))]
1022pub fn diff_entries(
1023 source_entries: &HashMap<String, LDAPEntry>,
1024 destination_entries: &HashMap<String, LDAPEntry>,
1025 source_base_dn: &str,
1026 destination_base_dn: &str,
1027 ignore_object_classes: &[String],
1028 ignore_attributes: &[String],
1029 source_ldap_schema: &LDAPSchema,
1030 add: bool,
1031 update: bool,
1032 delete: bool,
1033) -> Result<Vec<LDAPOperation>, LdapOperationError> {
1034 let diff = Diff::diff(source_entries, destination_entries);
1035 tracing::trace!("Diff:\n{:#?}", diff);
1036 let mut ldap_operations: Vec<LDAPOperation> = vec![];
1037 for (altered_dn, change) in diff.altered {
1038 tracing::trace!("Processing altered DN {}", altered_dn);
1039 let source_entry: Option<&LDAPEntry> = source_entries.get(&altered_dn);
1040 let destination_entry: Option<&LDAPEntry> = destination_entries.get(&altered_dn);
1041 if let Some(source_entry) = source_entry {
1042 let mut ldap_mods: Vec<ldap3::Mod<String>> = vec![];
1043 let mut ldap_bin_mods: Vec<ldap3::Mod<Vec<u8>>> = vec![];
1044 for (attr_name, attr_value_changes) in &change.attrs.altered {
1045 if ignore_attributes.contains(attr_name) {
1046 continue;
1047 }
1048 for attr_value_change in &attr_value_changes.0 {
1049 match attr_value_change {
1050 VecDiffType::Removed { .. }
1051 | VecDiffType::Inserted { .. }
1052 | VecDiffType::Altered { .. } => {
1053 let m = mod_value(
1054 attr_name,
1055 source_entry,
1056 source_ldap_schema,
1057 source_base_dn,
1058 destination_entry,
1059 destination_base_dn,
1060 ignore_object_classes,
1061 )?;
1062 if let Some(m) = m
1063 && !ldap_mods.contains(&m)
1064 {
1065 ldap_mods.push(m);
1066 }
1067 }
1068 }
1069 }
1070 }
1071 for attr_name in &change.attrs.removed {
1072 if ignore_attributes.contains(attr_name) {
1073 continue;
1074 }
1075 let mut replacement_values = HashSet::from_iter(
1076 source_entry
1077 .attrs
1078 .get(attr_name)
1079 .ok_or(LdapOperationError::MissingAttribute(attr_name.clone()))?
1080 .iter()
1081 .cloned(),
1082 );
1083 if attr_name == "objectClass" {
1084 for io in ignore_object_classes {
1085 replacement_values.remove(io);
1086 }
1087 }
1088 let attr_type_syntax = source_ldap_schema
1089 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
1090 tracing::trace!(
1091 "Attribute type syntax for deleted attribute {}: {:#?}",
1092 attr_name,
1093 attr_type_syntax
1094 );
1095 if let Some(syntax) = attr_type_syntax
1096 && (*DN_SYNTAX_OID)
1097 .as_ref()
1098 .map_err(|e| OIDError(*e))?
1099 .eq(syntax)
1100 {
1101 tracing::trace!(
1102 "Replacing base DN {} with base DN {}",
1103 source_base_dn,
1104 destination_base_dn
1105 );
1106 replacement_values = replacement_values
1107 .into_iter()
1108 .map(|s| s.replace(source_base_dn, destination_base_dn))
1109 .collect();
1110 }
1111 ldap_mods.push(ldap3::Mod::Add(attr_name.to_string(), replacement_values));
1112 }
1113 for (attr_name, attr_value_changes) in &change.bin_attrs.altered {
1114 if ignore_attributes.contains(attr_name) {
1115 continue;
1116 }
1117 for attr_value_change in &attr_value_changes.0 {
1118 match attr_value_change {
1119 VecDiffType::Removed { .. }
1120 | VecDiffType::Inserted { .. }
1121 | VecDiffType::Altered { .. } => {
1122 if let Some(values) = source_entry.bin_attrs.get(attr_name) {
1123 let replace_mod = ldap3::Mod::Replace(
1124 attr_name.as_bytes().to_vec(),
1125 HashSet::from_iter(values.iter().cloned()),
1126 );
1127 if !ldap_bin_mods.contains(&replace_mod) {
1128 ldap_bin_mods.push(replace_mod);
1129 }
1130 } else {
1131 ldap_bin_mods.push(ldap3::Mod::Delete(
1132 attr_name.as_bytes().to_vec(),
1133 HashSet::new(),
1134 ));
1135 }
1136 }
1137 }
1138 }
1139 }
1140 for attr_name in &change.bin_attrs.removed {
1141 if ignore_attributes.contains(attr_name) {
1142 continue;
1143 }
1144 ldap_bin_mods.push(ldap3::Mod::Add(
1145 attr_name.as_bytes().to_vec(),
1146 HashSet::from_iter(
1147 source_entry
1148 .bin_attrs
1149 .get(attr_name)
1150 .ok_or(LdapOperationError::MissingAttribute(attr_name.clone()))?
1151 .iter()
1152 .cloned(),
1153 ),
1154 ));
1155 }
1156 if update && !(ldap_mods.is_empty() && ldap_bin_mods.is_empty()) {
1157 ldap_operations.push(LDAPOperation::Modify {
1158 dn: source_entry.dn.clone(),
1159 mods: ldap_mods,
1160 bin_mods: ldap_bin_mods,
1161 });
1162 }
1163 } else if delete {
1164 ldap_operations.push(LDAPOperation::Delete {
1165 dn: altered_dn.clone(),
1166 });
1167 }
1168 }
1169 for removed_dn in diff.removed {
1170 if add {
1171 let mut new_entry = source_entries[&removed_dn].clone();
1172 for ia in ignore_attributes {
1173 new_entry.attrs.remove(ia);
1174 new_entry.bin_attrs.remove(ia);
1175 }
1176 if let Some((k, v)) = new_entry.attrs.remove_entry("objectClass") {
1177 let ioc = &ignore_object_classes;
1178 let new_v = v.into_iter().filter(|x| !ioc.contains(x)).collect();
1179 new_entry.attrs.insert(k, new_v);
1180 }
1181 for (attr_name, attr_values) in &mut new_entry.attrs {
1182 let attr_type_syntax = source_ldap_schema
1183 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
1184 tracing::trace!(
1185 "Attribute type syntax for attribute {} in deleted entry {}: {:#?}",
1186 attr_name,
1187 removed_dn,
1188 attr_type_syntax
1189 );
1190 if let Some(syntax) = attr_type_syntax
1191 && (*DN_SYNTAX_OID)
1192 .as_ref()
1193 .map_err(|e| OIDError(*e))?
1194 .eq(syntax)
1195 {
1196 tracing::trace!(
1197 "Replacing base DN {} with base DN {}",
1198 source_base_dn,
1199 destination_base_dn
1200 );
1201 for s in attr_values.iter_mut() {
1202 *s = s.replace(source_base_dn, destination_base_dn);
1203 }
1204 }
1205 }
1206 ldap_operations.push(LDAPOperation::Add(new_entry));
1207 }
1208 }
1209
1210 ldap_operations.sort_by(|a, b| {
1211 a.operation_apply_cmp(b)
1212 .unwrap_or(std::cmp::Ordering::Equal)
1213 });
1214
1215 Ok(ldap_operations)
1216}