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 attribute_type_parser, ldap_syntax_parser, matching_rule_parser, matching_rule_use_parser,
9 object_class_parser, AttributeType, LDAPSchema, LDAPSyntax, MatchingRule, MatchingRuleUse,
10 ObjectClass,
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 = std::fs::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 = std::fs::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 = std::fs::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!("Building of ConnectParameters based on OpenLDAP config files and substituting default values for missing values failed: {}", err);
290 Err(ConnectError::ParametersBuilderError(err))
291 }
292 }
293 }
294 }
295}
296
297#[instrument]
304pub async fn connect_with_parameters(
305 connect_parameters: ConnectParameters,
306) -> Result<(Ldap, std::string::String), ConnectError> {
307 let mut client_cert_contents = Vec::new();
308 {
309 let mut file = File::open(connect_parameters.client_cert_path)?;
310 file.read_to_end(&mut client_cert_contents)?;
311 }
312 let client_cert = X509::from_pem(&client_cert_contents)?;
313 let mut client_key_contents = Vec::new();
314 {
315 let mut file = File::open(connect_parameters.client_key_path)?;
316 file.read_to_end(&mut client_key_contents)?;
317 }
318 let client_key = PKey::private_key_from_pem(&client_key_contents)?;
319 let p12_password = "client";
320 let p12 = Pkcs12::builder()
321 .name("client")
322 .pkey(&client_key)
323 .cert(&client_cert)
324 .build2(p12_password)?;
325 let p12_contents = p12.to_der()?;
326 let mut ca_cert_contents = Vec::new();
327 {
328 let mut file = File::open(connect_parameters.ca_cert_path)?;
329 file.read_to_end(&mut ca_cert_contents)?;
330 }
331 let identity = Identity::from_pkcs12(&p12_contents, p12_password)?;
332 let ca_certificate = Certificate::from_pem(&ca_cert_contents)?;
333 let connector = TlsConnector::builder()
334 .identity(identity)
335 .add_root_certificate(ca_certificate)
336 .build()?;
337 let ldap_settings = LdapConnSettings::new().set_connector(connector);
338 let (ldap_conn_async, mut ldap) =
339 LdapConnAsync::with_settings(ldap_settings, &connect_parameters.url.clone()).await?;
340 ldap3::drive!(ldap_conn_async);
341 ldap.sasl_external_bind().await?;
342 let (exop, _res) = ldap.extended(WhoAmI).await?.success()?;
343 let who_am_i: WhoAmIResp = exop.parse();
344 let re = Regex::new(r"^.*,ou=[a-z]+,")?;
345 let base_dn = re.replace_all(&who_am_i.authzid, "").to_string();
346 Ok((ldap, base_dn))
347}
348
349#[derive(Debug, Error)]
351pub enum LdapOperationError {
352 #[error("ldap3 Ldap error: {0}")]
354 LdapError(#[from] ldap3::LdapError),
355 #[error("OID error: {0}")]
357 OIDError(#[from] OIDError),
358 #[error("Missing expected attribute: {0}")]
360 MissingAttribute(String),
361}
362
363pub async fn ldap_search<'a, S: AsRef<str> + Clone + Display + Debug + Send + Sync>(
370 ldap: &mut Ldap,
371 base: &str,
372 scope: Scope,
373 filter: &str,
374 attrs: Vec<S>,
375) -> Result<Box<dyn Iterator<Item = SearchEntry> + 'a>, LdapOperationError> {
376 let search_result = ldap.search(base, scope, filter, attrs.clone()).await?;
377 let SearchResult(_rs, res) = &search_result;
378 if res.rc != 0 {
379 tracing::debug!(
380 "Non-zero return code {} in LDAP query\n base: {}\n scope: {:?}\n filter: {}\n attrs: {:#?}",
381 res.rc,
382 base,
383 scope,
384 filter,
385 attrs
386 );
387 tracing::debug!(
388 "ldapsearch -Q -LLL -o ldif-wrap=no -b '{}' -s {} '{}' {}",
389 base,
390 format!("{scope:?}").to_lowercase(),
391 filter,
392 itertools::join(attrs.iter(), " ")
393 );
394 }
395 let (rs, _res) = search_result.success()?;
396 Ok(Box::new(rs.into_iter().map(SearchEntry::construct)))
397}
398
399#[derive(Debug)]
402pub struct OIDError(oid::ObjectIdentifierError);
403
404impl Display for OIDError {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 write!(f, "Error parsing OID: {:?}", self.0)
407 }
408}
409
410impl std::error::Error for OIDError {
411 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
412 None
413 }
414}
415
416#[instrument(skip(ldap))]
423pub async fn query_root_dse(ldap: &mut Ldap) -> Result<Option<RootDSE>, LdapOperationError> {
424 let mut it = ldap_search(
425 ldap,
426 "",
427 Scope::Base,
428 "(objectClass=*)",
429 vec![
430 "supportedLDAPVersion",
431 "supportedControl",
432 "supportedExtension",
433 "supportedFeatures",
434 "supportedSASLMechanisms",
435 "configContext",
436 "namingContexts",
437 "subschemaSubentry",
438 ],
439 )
440 .await?;
441 if let Some(entry) = it.next() {
442 let supported_ldap_version = entry
443 .attrs
444 .get("supportedLDAPVersion")
445 .ok_or(LdapOperationError::MissingAttribute(
446 "supportedLDAPVersion".to_string(),
447 ))?
448 .first()
449 .ok_or(LdapOperationError::MissingAttribute(
450 "supportedLDAPVersion".to_string(),
451 ))?;
452 let supported_controls =
453 entry
454 .attrs
455 .get("supportedControl")
456 .ok_or(LdapOperationError::MissingAttribute(
457 "supportedControl".to_string(),
458 ))?;
459 let supported_extensions =
460 entry
461 .attrs
462 .get("supportedExtension")
463 .ok_or(LdapOperationError::MissingAttribute(
464 "supportedExtension".to_string(),
465 ))?;
466 let supported_features =
467 entry
468 .attrs
469 .get("supportedFeatures")
470 .ok_or(LdapOperationError::MissingAttribute(
471 "supportedFeatures".to_string(),
472 ))?;
473 let supported_sasl_mechanisms = entry.attrs.get("supportedSASLMechanisms").ok_or(
474 LdapOperationError::MissingAttribute("supportedSASLMechanisms".to_string()),
475 )?;
476 let config_context = entry
477 .attrs
478 .get("configContext")
479 .ok_or(LdapOperationError::MissingAttribute(
480 "configContext".to_string(),
481 ))?
482 .first()
483 .ok_or(LdapOperationError::MissingAttribute(
484 "configContext".to_string(),
485 ))?;
486 let naming_contexts =
487 entry
488 .attrs
489 .get("namingContexts")
490 .ok_or(LdapOperationError::MissingAttribute(
491 "namingContexts".to_string(),
492 ))?;
493 let subschema_subentry = entry
494 .attrs
495 .get("subschemaSubentry")
496 .ok_or(LdapOperationError::MissingAttribute(
497 "subschemaSubentry".to_string(),
498 ))?
499 .first()
500 .ok_or(LdapOperationError::MissingAttribute(
501 "subschemaSubentry".to_string(),
502 ))?;
503 return Ok(Some(RootDSE {
504 supported_ldap_version: supported_ldap_version.to_string(),
505 supported_controls: supported_controls
506 .iter()
507 .map(|x| x.clone().try_into())
508 .collect::<Result<_, _>>()
509 .map_err(OIDError)?,
510 supported_extensions: supported_extensions
511 .iter()
512 .map(|x| x.clone().try_into())
513 .collect::<Result<_, _>>()
514 .map_err(OIDError)?,
515 supported_features: supported_features
516 .iter()
517 .map(|x| x.clone().try_into())
518 .collect::<Result<_, _>>()
519 .map_err(OIDError)?,
520 supported_sasl_mechanisms: supported_sasl_mechanisms.to_vec(),
521 config_context: config_context.to_string(),
522 naming_contexts: naming_contexts.to_vec(),
523 subschema_subentry: subschema_subentry.to_string(),
524 }));
525 }
526 Ok(None)
527}
528
529#[derive(Debug, Error)]
531pub enum LdapSchemaError {
532 #[error("Ldap operation error: {0}")]
534 LdapOperationError(#[from] LdapOperationError),
535 #[error("chumsky parser error: {0}")]
537 ChumskyError(#[from] ChumskyError<chumsky::error::Rich<'static, char>>),
538}
539
540#[instrument(skip(ldap))]
548pub async fn query_ldap_schema(ldap: &mut Ldap) -> Result<Option<LDAPSchema>, LdapSchemaError> {
549 if let Some(root_dse) = query_root_dse(ldap).await? {
550 let mut it = ldap_search(
551 ldap,
552 &root_dse.subschema_subentry,
553 Scope::Base,
554 "(objectClass=*)",
555 vec![
556 "ldapSyntaxes",
557 "matchingRules",
558 "matchingRuleUse",
559 "attributeTypes",
560 "objectClasses",
561 ],
562 )
563 .await?;
564
565 if let Some(entry) = it.next() {
566 let ldap_syntaxes = entry
567 .attrs
568 .get("ldapSyntaxes")
569 .ok_or(LdapOperationError::MissingAttribute(
570 "ldapSyntaxes".to_string(),
571 ))?
572 .iter()
573 .map(|x| match ldap_syntax_parser().parse(x.as_str()).into_output_errors() {
574 (Some(ldap_syntax), _) => Ok(ldap_syntax),
575 (_, errs) => Err(ChumskyError {
576 description: "ldap syntax".to_string(),
577 source: x.to_string(),
578 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
579 }),
580 })
581 .collect::<Result<Vec<LDAPSyntax>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
582 let matching_rules = entry
583 .attrs
584 .get("matchingRules")
585 .ok_or(LdapOperationError::MissingAttribute(
586 "matchingRules".to_string(),
587 ))?
588 .iter()
589 .map(
590 |x| match matching_rule_parser().parse(x.as_str()).into_output_errors() {
591 (Some(matching_rule), _) => Ok(matching_rule),
592 (_, errs) => Err(ChumskyError {
593 description: "matching rule".to_string(),
594 source: x.to_string(),
595 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
596 }),
597 },
598 )
599 .collect::<Result<Vec<MatchingRule>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
600 let matching_rule_use =
601 entry
602 .attrs
603 .get("matchingRuleUse")
604 .ok_or(LdapOperationError::MissingAttribute(
605 "matchingRuleUse".to_string(),
606 ))?
607 .iter()
608 .map(|x| {
609 match matching_rule_use_parser()
610 .parse(x.as_str())
611 .into_output_errors()
612 {
613 (Some(matching_rule_use), _) => Ok(matching_rule_use),
614 (_, errs) => Err(ChumskyError {
615 description: "matching rule use".to_string(),
616 source: x.to_string(),
617 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
618 }),
619 }
620 })
621 .collect::<Result<
622 Vec<MatchingRuleUse>,
623 ChumskyError<chumsky::error::Rich<'static, char>>,
624 >>()?;
625 let attribute_types = entry
626 .attrs
627 .get("attributeTypes")
628 .ok_or(LdapOperationError::MissingAttribute(
629 "attributeTypes".to_string(),
630 ))?
631 .iter()
632 .map(
633 |x| match attribute_type_parser().parse(x.as_str()).into_output_errors() {
634 (Some(attribute_type), _) => Ok(attribute_type),
635 (_, errs) => Err(ChumskyError {
636 description: "attribute type".to_string(),
637 source: x.to_string(),
638 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
639 }),
640 },
641 )
642 .collect::<Result<Vec<AttributeType>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
643 let object_classes = entry
644 .attrs
645 .get("objectClasses")
646 .ok_or(LdapOperationError::MissingAttribute(
647 "objectClasses".to_string(),
648 ))?
649 .iter()
650 .map(|x| match object_class_parser().parse(x.as_str()).into_output_errors() {
651 (Some(object_class), _) => Ok(object_class),
652 (_, errs) => Err(ChumskyError {
653 description: "object class".to_string(),
654 source: x.to_string(),
655 errors: errs.into_iter().map(|e| e.into_owned()).collect(),
656 }),
657 })
658 .collect::<Result<Vec<ObjectClass>, ChumskyError<chumsky::error::Rich<'static, char>>>>()?;
659 return Ok(Some(LDAPSchema {
660 ldap_syntaxes,
661 matching_rules,
662 matching_rule_use,
663 attribute_types,
664 object_classes,
665 }));
666 }
667 }
668 Ok(None)
669}
670
671pub fn success_or_noop_success(
677 ldap_result: ldap3::LdapResult,
678) -> ldap3::result::Result<ldap3::LdapResult> {
679 if ldap_result.rc == 0 || ldap_result.rc == 16654 {
681 Ok(ldap_result)
682 } else {
683 Err(ldap3::LdapError::from(ldap_result))
684 }
685}
686
687#[instrument(skip(ldap))]
693pub async fn delete_recursive(
694 ldap: &mut Ldap,
695 dn: &str,
696 controls: Vec<ldap3::controls::RawControl>,
697) -> Result<(), LdapOperationError> {
698 tracing::debug!("Deleting {} recursively", dn);
699 let it = ldap_search(
700 ldap,
701 dn,
702 Scope::Subtree,
703 "(objectClass=*)",
704 Vec::<String>::new(),
705 )
706 .await?;
707 let mut entries = vec![];
708 for entry in it {
709 tracing::debug!("Found child entry to delete {}", entry.dn);
710 entries.push(entry.dn);
711 }
712 entries.sort_by_key(|b| std::cmp::Reverse(b.len()));
713 for dn in entries {
714 tracing::debug!("Deleting child entry {}", dn);
715 success_or_noop_success(ldap.with_controls(controls.to_owned()).delete(&dn).await?)?;
716 }
717 Ok(())
718}
719
720pub fn mods_as_bin_mods<'a, T>(mods: T) -> Vec<ldap3::Mod<Vec<u8>>>
723where
724 T: IntoIterator<Item = &'a ldap3::Mod<String>>,
725{
726 let mut result: Vec<ldap3::Mod<Vec<u8>>> = vec![];
727 for m in mods {
728 match m {
729 ldap3::Mod::Add(k, v) => {
730 result.push(ldap3::Mod::Add(
731 k.as_bytes().to_vec(),
732 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
733 ));
734 }
735 ldap3::Mod::Delete(k, v) => {
736 result.push(ldap3::Mod::Delete(
737 k.as_bytes().to_vec(),
738 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
739 ));
740 }
741 ldap3::Mod::Replace(k, v) => {
742 result.push(ldap3::Mod::Replace(
743 k.as_bytes().to_vec(),
744 v.iter().map(|s| s.as_bytes().to_vec()).collect(),
745 ));
746 }
747 ldap3::Mod::Increment(k, v) => {
748 result.push(ldap3::Mod::Increment(
749 k.as_bytes().to_vec(),
750 v.as_bytes().to_vec(),
751 ));
752 }
753 }
754 }
755 result
756}
757
758#[instrument(skip(ldap, ldap_operations))]
768pub async fn apply_ldap_operations(
769 ldap: &mut Ldap,
770 ldap_base_dn: &str,
771 ldap_operations: &[LDAPOperation],
772 controls: Vec<ldap3::controls::RawControl>,
773) -> Result<(), LdapOperationError> {
774 tracing::debug!(
775 "The following operations use the LDAP controls: {:#?}",
776 controls
777 );
778 for op in ldap_operations {
779 match op {
780 LDAPOperation::Add(LDAPEntry {
781 dn,
782 attrs,
783 bin_attrs,
784 }) => {
785 let full_dn = format!("{dn},{ldap_base_dn}");
786 tracing::debug!(
787 "Adding LDAP entry at {} with attributes\n{:#?}\nand binary attributes\n{:#?}",
788 &full_dn,
789 attrs,
790 bin_attrs
791 );
792 let mut combined_attrs: Vec<(Vec<u8>, HashSet<Vec<u8>>)> = bin_attrs
795 .iter()
796 .map(|(k, v)| {
797 (
798 k.to_owned().as_bytes().to_vec(),
799 v.iter().map(|s| s.to_owned()).collect::<HashSet<Vec<u8>>>(),
800 )
801 })
802 .collect();
803 combined_attrs.extend(attrs.iter().map(|(k, v)| {
804 (
805 k.to_owned().as_bytes().to_vec(),
806 v.iter()
807 .map(|s| s.as_bytes().to_vec())
808 .collect::<HashSet<Vec<u8>>>(),
809 )
810 }));
811 ldap.with_controls(controls.to_owned())
812 .add(&full_dn, combined_attrs)
813 .await?
814 .success()?;
815 }
816 LDAPOperation::Delete { dn } => {
817 let full_dn = format!("{dn},{ldap_base_dn}");
818 tracing::debug!("Deleting LDAP entry at {}", &full_dn);
819 delete_recursive(ldap, &full_dn, controls.to_owned()).await?;
820 }
821 LDAPOperation::Modify { dn, mods, bin_mods } => {
822 let full_dn = format!("{dn},{ldap_base_dn}");
823 tracing::debug!("Modifying LDAP entry at {} with modifications\n{:#?}\nand binary modifications\n{:#?}", &full_dn, mods, bin_mods);
824 let mut combined_mods = bin_mods.to_owned();
825 combined_mods.extend(mods_as_bin_mods(mods));
826 ldap.with_controls(controls.to_owned())
827 .modify(&full_dn, combined_mods.to_vec())
828 .await?
829 .success()?;
830 }
831 }
832 }
833
834 Ok(())
835}
836
837#[instrument(skip(ldap, entries))]
844pub async fn search_entries(
845 ldap: &mut Ldap,
846 base_dn: &str,
847 search_base: &str,
848 scope: ldap3::Scope,
849 filter: &str,
850 attrs: &[String],
851 entries: &mut HashMap<String, LDAPEntry>,
852) -> Result<(), LdapOperationError> {
853 let it = ldap_search(
854 ldap,
855 &format!("{search_base},{base_dn}"),
856 scope,
857 filter,
858 attrs.to_owned(),
859 )
860 .await?;
861 for entry in it {
862 tracing::debug!("Found entry {}", entry.dn);
863 if let Some(s) = entry.dn.strip_suffix(&format!(",{}", &base_dn)) {
864 entries.insert(
865 s.to_string(),
866 LDAPEntry {
867 dn: s.to_string(),
868 attrs: entry.attrs,
869 bin_attrs: entry.bin_attrs,
870 },
871 );
872 } else {
873 tracing::error!(
874 "Failed to remove base dn {} from entry DN {}",
875 base_dn,
876 entry.dn
877 );
878 }
879 }
880 Ok(())
881}
882
883#[instrument(skip(
890 source_entry,
891 source_ldap_schema,
892 source_base_dn,
893 destination_entry,
894 destination_base_dn,
895 ignore_object_classes,
896))]
897pub fn mod_value(
898 attr_name: &str,
899 source_entry: &LDAPEntry,
900 source_ldap_schema: &LDAPSchema,
901 source_base_dn: &str,
902 destination_entry: Option<&LDAPEntry>,
903 destination_base_dn: &str,
904 ignore_object_classes: &[String],
905) -> Result<Option<ldap3::Mod<String>>, LdapOperationError> {
906 if let Some(values) = source_entry.attrs.get(attr_name) {
907 let mut replacement_values = HashSet::from_iter(values.iter().cloned());
908 if attr_name == "objectClass" {
909 for io in ignore_object_classes {
910 replacement_values.remove(io);
911 }
912 }
913 let attr_type_syntax =
914 source_ldap_schema.find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
915 tracing::trace!(
916 "Attribute type syntax for altered attribute {}: {:#?}",
917 attr_name,
918 attr_type_syntax
919 );
920 if let Some(syntax) = attr_type_syntax {
921 if (*DN_SYNTAX_OID)
922 .as_ref()
923 .map_err(|e| OIDError(*e))?
924 .eq(syntax)
925 {
926 tracing::trace!(
927 "Replacing base DN {} with base DN {}",
928 source_base_dn,
929 destination_base_dn
930 );
931 replacement_values = replacement_values
932 .into_iter()
933 .map(|s| s.replace(source_base_dn, destination_base_dn))
934 .collect();
935 }
936 }
937 if let Some(destination_entry) = destination_entry {
938 if let Some(destination_values) = destination_entry.attrs.get(attr_name) {
939 let mut replacement_values_sorted: Vec<String> =
940 replacement_values.iter().cloned().collect();
941 replacement_values_sorted.sort();
942 let mut destination_values: Vec<String> = destination_values.to_vec();
943 destination_values.sort();
944 tracing::trace!("Checking if replacement values and destination values are identical (case sensitive):\n{:#?}\n{:#?}", destination_values, replacement_values_sorted);
945 if replacement_values_sorted == destination_values {
946 tracing::trace!("Skipping attribute {} because replacement values and destination values are identical (case sensitive)", attr_name);
947 return Ok(None);
948 }
949 let attr_type_equality = source_ldap_schema
950 .find_attribute_type_property(attr_name, |at| at.equality.as_ref());
951 tracing::trace!(
952 "Attribute type equality for altered attribute {}: {:#?}",
953 attr_name,
954 attr_type_equality
955 );
956 if let Some(equality) = &attr_type_equality {
957 if equality.describes_case_insensitive_match() {
958 let mut lower_destination_values: Vec<String> = destination_values
959 .iter()
960 .map(|s| s.to_lowercase())
961 .collect();
962 lower_destination_values.sort();
963 let mut lower_replacement_values: Vec<String> = replacement_values
964 .iter()
965 .map(|s| s.to_lowercase())
966 .collect();
967 lower_replacement_values.sort();
968 tracing::trace!("Checking if replacement values and destination values are identical (case insensitive):\n{:#?}\n{:#?}", lower_destination_values, lower_replacement_values);
969 if lower_destination_values == lower_replacement_values {
970 tracing::trace!("Skipping attribute {} because replacement values and destination values are identical (case insensitive)", attr_name);
971 return Ok(None);
972 }
973 }
974 }
975 }
976 }
977 Ok(Some(ldap3::Mod::Replace(
978 attr_name.to_string(),
979 replacement_values,
980 )))
981 } else {
982 Ok(Some(ldap3::Mod::Delete(
983 attr_name.to_string(),
984 HashSet::new(),
985 )))
986 }
987}
988
989#[expect(
997 clippy::too_many_arguments,
998 reason = "factoring parameters into objects is not sensible since this is basically standalone without many connections to the rest of the crate"
999)]
1000#[instrument(skip(source_ldap_schema))]
1001pub fn diff_entries(
1002 source_entries: &HashMap<String, LDAPEntry>,
1003 destination_entries: &HashMap<String, LDAPEntry>,
1004 source_base_dn: &str,
1005 destination_base_dn: &str,
1006 ignore_object_classes: &[String],
1007 ignore_attributes: &[String],
1008 source_ldap_schema: &LDAPSchema,
1009 add: bool,
1010 update: bool,
1011 delete: bool,
1012) -> Result<Vec<LDAPOperation>, LdapOperationError> {
1013 let diff = Diff::diff(source_entries, destination_entries);
1014 tracing::trace!("Diff:\n{:#?}", diff);
1015 let mut ldap_operations: Vec<LDAPOperation> = vec![];
1016 for (altered_dn, change) in diff.altered {
1017 tracing::trace!("Processing altered DN {}", altered_dn);
1018 let source_entry: Option<&LDAPEntry> = source_entries.get(&altered_dn);
1019 let destination_entry: Option<&LDAPEntry> = destination_entries.get(&altered_dn);
1020 if let Some(source_entry) = source_entry {
1021 let mut ldap_mods: Vec<ldap3::Mod<String>> = vec![];
1022 let mut ldap_bin_mods: Vec<ldap3::Mod<Vec<u8>>> = vec![];
1023 for (attr_name, attr_value_changes) in &change.attrs.altered {
1024 if ignore_attributes.contains(attr_name) {
1025 continue;
1026 }
1027 for attr_value_change in &attr_value_changes.0 {
1028 match attr_value_change {
1029 VecDiffType::Removed { .. }
1030 | VecDiffType::Inserted { .. }
1031 | VecDiffType::Altered { .. } => {
1032 let m = mod_value(
1033 attr_name,
1034 source_entry,
1035 source_ldap_schema,
1036 source_base_dn,
1037 destination_entry,
1038 destination_base_dn,
1039 ignore_object_classes,
1040 )?;
1041 if let Some(m) = m {
1042 if !ldap_mods.contains(&m) {
1043 ldap_mods.push(m);
1044 }
1045 }
1046 }
1047 }
1048 }
1049 }
1050 for attr_name in &change.attrs.removed {
1051 if ignore_attributes.contains(attr_name) {
1052 continue;
1053 }
1054 let mut replacement_values = HashSet::from_iter(
1055 source_entry
1056 .attrs
1057 .get(attr_name)
1058 .ok_or(LdapOperationError::MissingAttribute(attr_name.clone()))?
1059 .iter()
1060 .cloned(),
1061 );
1062 if attr_name == "objectClass" {
1063 for io in ignore_object_classes {
1064 replacement_values.remove(io);
1065 }
1066 }
1067 let attr_type_syntax = source_ldap_schema
1068 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
1069 tracing::trace!(
1070 "Attribute type syntax for deleted attribute {}: {:#?}",
1071 attr_name,
1072 attr_type_syntax
1073 );
1074 if let Some(syntax) = attr_type_syntax {
1075 if (*DN_SYNTAX_OID)
1076 .as_ref()
1077 .map_err(|e| OIDError(*e))?
1078 .eq(syntax)
1079 {
1080 tracing::trace!(
1081 "Replacing base DN {} with base DN {}",
1082 source_base_dn,
1083 destination_base_dn
1084 );
1085 replacement_values = replacement_values
1086 .into_iter()
1087 .map(|s| s.replace(source_base_dn, destination_base_dn))
1088 .collect();
1089 }
1090 }
1091 ldap_mods.push(ldap3::Mod::Add(attr_name.to_string(), replacement_values));
1092 }
1093 for (attr_name, attr_value_changes) in &change.bin_attrs.altered {
1094 if ignore_attributes.contains(attr_name) {
1095 continue;
1096 }
1097 for attr_value_change in &attr_value_changes.0 {
1098 match attr_value_change {
1099 VecDiffType::Removed { .. }
1100 | VecDiffType::Inserted { .. }
1101 | VecDiffType::Altered { .. } => {
1102 if let Some(values) = source_entry.bin_attrs.get(attr_name) {
1103 let replace_mod = ldap3::Mod::Replace(
1104 attr_name.as_bytes().to_vec(),
1105 HashSet::from_iter(values.iter().cloned()),
1106 );
1107 if !ldap_bin_mods.contains(&replace_mod) {
1108 ldap_bin_mods.push(replace_mod);
1109 }
1110 } else {
1111 ldap_bin_mods.push(ldap3::Mod::Delete(
1112 attr_name.as_bytes().to_vec(),
1113 HashSet::new(),
1114 ));
1115 }
1116 }
1117 }
1118 }
1119 }
1120 for attr_name in &change.bin_attrs.removed {
1121 if ignore_attributes.contains(attr_name) {
1122 continue;
1123 }
1124 ldap_bin_mods.push(ldap3::Mod::Add(
1125 attr_name.as_bytes().to_vec(),
1126 HashSet::from_iter(
1127 source_entry
1128 .bin_attrs
1129 .get(attr_name)
1130 .ok_or(LdapOperationError::MissingAttribute(attr_name.clone()))?
1131 .iter()
1132 .cloned(),
1133 ),
1134 ));
1135 }
1136 if update && !(ldap_mods.is_empty() && ldap_bin_mods.is_empty()) {
1137 ldap_operations.push(LDAPOperation::Modify {
1138 dn: source_entry.dn.clone(),
1139 mods: ldap_mods,
1140 bin_mods: ldap_bin_mods,
1141 });
1142 }
1143 } else if delete {
1144 ldap_operations.push(LDAPOperation::Delete {
1145 dn: altered_dn.clone(),
1146 });
1147 }
1148 }
1149 for removed_dn in diff.removed {
1150 if add {
1151 let mut new_entry = source_entries[&removed_dn].clone();
1152 for ia in ignore_attributes {
1153 new_entry.attrs.remove(ia);
1154 new_entry.bin_attrs.remove(ia);
1155 }
1156 if let Some((k, v)) = new_entry.attrs.remove_entry("objectClass") {
1157 let ioc = &ignore_object_classes;
1158 let new_v = v.into_iter().filter(|x| !ioc.contains(x)).collect();
1159 new_entry.attrs.insert(k, new_v);
1160 }
1161 for (attr_name, attr_values) in &mut new_entry.attrs {
1162 let attr_type_syntax = source_ldap_schema
1163 .find_attribute_type_property(attr_name, |at| at.syntax.as_ref());
1164 tracing::trace!(
1165 "Attribute type syntax for attribute {} in deleted entry {}: {:#?}",
1166 attr_name,
1167 removed_dn,
1168 attr_type_syntax
1169 );
1170 if let Some(syntax) = attr_type_syntax {
1171 if (*DN_SYNTAX_OID)
1172 .as_ref()
1173 .map_err(|e| OIDError(*e))?
1174 .eq(syntax)
1175 {
1176 tracing::trace!(
1177 "Replacing base DN {} with base DN {}",
1178 source_base_dn,
1179 destination_base_dn
1180 );
1181 for s in attr_values.iter_mut() {
1182 *s = s.replace(source_base_dn, destination_base_dn);
1183 }
1184 }
1185 }
1186 }
1187 ldap_operations.push(LDAPOperation::Add(new_entry));
1188 }
1189 }
1190
1191 ldap_operations.sort_by(|a, b| {
1192 a.operation_apply_cmp(b)
1193 .unwrap_or(std::cmp::Ordering::Equal)
1194 });
1195
1196 Ok(ldap_operations)
1197}