Skip to main content

ldap3_client/
lib.rs

1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::panic)]
7#![deny(clippy::await_holding_lock)]
8#![deny(clippy::needless_pass_by_value)]
9#![deny(clippy::trivially_copy_pass_by_ref)]
10// We allow expect since it forces good error messages at the least.
11#![allow(clippy::expect_used)]
12
13use base64::{Engine as _, engine::general_purpose};
14use futures_util::sink::SinkExt;
15use futures_util::stream::StreamExt;
16use ldap3_proto::LdapCodec;
17use ldap3_proto::proto::*;
18use rustls_platform_verifier::ConfigVerifierExt;
19use serde::{Deserialize, Serialize};
20use std::borrow::Borrow;
21use std::collections::{BTreeMap, BTreeSet};
22use std::fmt;
23use std::sync::Arc;
24use tokio::io::{ReadHalf, WriteHalf};
25use tokio::net::TcpStream;
26use tokio::time;
27
28use tokio_rustls::{
29    TlsConnector,
30    client::TlsStream,
31    rustls::Error as RustlsError,
32    rustls::client::{ClientConfig, danger::*},
33    rustls::pki_types::{CertificateDer, ServerName, UnixTime},
34    rustls::{DigitallySignedStruct, SignatureScheme},
35};
36
37use tokio_util::codec::{FramedRead, FramedWrite};
38use tracing::{error, info, trace, warn};
39use url::{Host, Url};
40use uuid::Uuid;
41
42pub use ldap3_proto::filter;
43pub use ldap3_proto::proto;
44pub use search::LdapSearchResult;
45pub use syncrepl::{LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue};
46pub use tokio::time::Duration;
47
48mod addirsync;
49mod search;
50mod syncrepl;
51
52#[non_exhaustive]
53#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
54#[repr(i32)]
55pub enum LdapError {
56    InvalidUrl = -1,
57    LdapiNotSupported = -2,
58    UseCldapTool = -3,
59    ResolverError = -4,
60    ConnectError = -5,
61    TlsError = -6,
62    PasswordNotFound = -7,
63    AnonymousInvalidState = -8,
64    TransportWriteError = -9,
65    TransportReadError = -10,
66    InvalidProtocolState = -11,
67    FileIOError = -12,
68
69    UnavailableCriticalExtension = 12,
70    InvalidCredentials = 49,
71    InsufficentAccessRights = 50,
72    UnwillingToPerform = 53,
73    EsyncRefreshRequired = 4096,
74    NotImplemented = 9999,
75}
76
77impl From<LdapResultCode> for LdapError {
78    fn from(code: LdapResultCode) -> Self {
79        match code {
80            LdapResultCode::InvalidCredentials => LdapError::InvalidCredentials,
81            LdapResultCode::InsufficentAccessRights => LdapError::InsufficentAccessRights,
82            LdapResultCode::EsyncRefreshRequired => LdapError::EsyncRefreshRequired,
83            LdapResultCode::UnavailableCriticalExtension => LdapError::UnavailableCriticalExtension,
84            LdapResultCode::UnwillingToPerform => LdapError::UnwillingToPerform,
85            err => {
86                error!("{:?} not implemented yet!!", err);
87                LdapError::NotImplemented
88            }
89        }
90    }
91}
92
93impl fmt::Display for LdapError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            LdapError::InvalidUrl => write!(f, "Invalid URL"),
97            LdapError::LdapiNotSupported => write!(f, "Ldapi Not Supported"),
98            LdapError::UseCldapTool => write!(f, "Use cldap tool for cldap:// urls"),
99            LdapError::ResolverError => write!(f, "Failed to resolve hostname or invalid ip"),
100            LdapError::ConnectError => write!(f, "Failed to connect to host"),
101            LdapError::TlsError => write!(f, "Failed to establish TLS"),
102            LdapError::PasswordNotFound => write!(f, "No password available for bind"),
103            LdapError::AnonymousInvalidState => write!(f, "Invalid Anonymous bind state"),
104            LdapError::InvalidProtocolState => {
105                write!(f, "The LDAP server sent a response we did not expect")
106            }
107            LdapError::FileIOError => {
108                write!(f, "An error occurred while accessing a file")
109            }
110            LdapError::TransportReadError => {
111                write!(f, "An error occurred reading from the transport")
112            }
113            LdapError::TransportWriteError => {
114                write!(f, "An error occurred writing to the transport")
115            }
116            LdapError::UnavailableCriticalExtension => {
117                write!(f, "An extension marked as critical was not available")
118            }
119            LdapError::InvalidCredentials => write!(f, "Invalid DN or Password"),
120            LdapError::InsufficentAccessRights => write!(f, "Insufficient Access"),
121            LdapError::UnwillingToPerform => write!(
122                f,
123                "Too many failures, server is unwilling to perform the operation."
124            ),
125            LdapError::EsyncRefreshRequired => write!(
126                f,
127                "An initial content sync is required. The current cookie should be considered invalid."
128            ),
129            LdapError::NotImplemented => write!(
130                f,
131                "An error occurred, but we haven't implemented code to handle this error yet."
132            ),
133        }
134    }
135}
136
137pub type LdapResult<T> = Result<T, LdapError>;
138
139enum LdapReadTransport {
140    Plain(FramedRead<ReadHalf<TcpStream>, LdapCodec>),
141    Tls(FramedRead<ReadHalf<TlsStream<TcpStream>>, LdapCodec>),
142}
143
144impl fmt::Debug for LdapReadTransport {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            LdapReadTransport::Plain(_) => f
148                .debug_struct("LdapReadTransport")
149                .field("type", &"plain")
150                .finish(),
151            LdapReadTransport::Tls(_) => f
152                .debug_struct("LdapReadTransport")
153                .field("type", &"tls")
154                .finish(),
155        }
156    }
157}
158
159enum LdapWriteTransport {
160    Plain(FramedWrite<WriteHalf<TcpStream>, LdapCodec>),
161    Tls(FramedWrite<WriteHalf<TlsStream<TcpStream>>, LdapCodec>),
162}
163
164impl fmt::Debug for LdapWriteTransport {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            LdapWriteTransport::Plain(_) => f
168                .debug_struct("LdapWriteTransport")
169                .field("type", &"plain")
170                .finish(),
171            LdapWriteTransport::Tls(_) => f
172                .debug_struct("LdapWriteTransport")
173                .field("type", &"tls")
174                .finish(),
175        }
176    }
177}
178
179impl LdapWriteTransport {
180    async fn send(&mut self, msg: LdapMsg) -> LdapResult<()> {
181        match self {
182            LdapWriteTransport::Plain(f) => f.send(msg).await.map_err(|e| {
183                info!(?e, "transport error");
184                LdapError::TransportWriteError
185            }),
186            LdapWriteTransport::Tls(f) => f.send(msg).await.map_err(|e| {
187                info!(?e, "transport error");
188                LdapError::TransportWriteError
189            }),
190        }
191    }
192}
193
194impl LdapReadTransport {
195    async fn next(&mut self) -> LdapResult<LdapMsg> {
196        match self {
197            LdapReadTransport::Plain(f) => f.next().await.transpose().map_err(|e| {
198                info!(?e, "transport error");
199                LdapError::TransportReadError
200            })?,
201            LdapReadTransport::Tls(f) => f.next().await.transpose().map_err(|e| {
202                info!(?e, "transport error");
203                LdapError::TransportReadError
204            })?,
205        }
206        .ok_or_else(|| {
207            info!("connection closed");
208            LdapError::TransportReadError
209        })
210    }
211}
212
213#[derive(Debug, Deserialize, Serialize)]
214pub struct LdapEntry {
215    pub dn: String,
216    pub attrs: BTreeMap<String, BTreeSet<String>>,
217}
218
219impl LdapEntry {
220    pub fn get_ava_single(&self, attr: &str) -> Option<&str> {
221        if let Some(ava) = self.attrs.get(attr) {
222            if ava.len() == 1 {
223                ava.iter().next().map(String::as_ref)
224            } else {
225                None
226            }
227        } else {
228            None
229        }
230    }
231
232    pub fn remove_ava_single(&mut self, attr: &str) -> Option<String> {
233        if let Some(ava) = self.attrs.remove(attr) {
234            if ava.len() == 1 {
235                ava.into_iter().next()
236            } else {
237                None
238            }
239        } else {
240            None
241        }
242    }
243
244    pub fn remove_ava(&mut self, attr: &str) -> Option<BTreeSet<String>> {
245        self.attrs.remove(attr)
246    }
247}
248
249impl From<LdapSearchResultEntry> for LdapEntry {
250    fn from(ent: LdapSearchResultEntry) -> Self {
251        let LdapSearchResultEntry { dn, attributes } = ent;
252
253        let attrs = attributes
254            .into_iter()
255            .map(|LdapPartialAttribute { atype, vals }| {
256                let atype = atype.to_lowercase();
257
258                let lower = atype == "objectclass";
259
260                let va = vals
261                    .into_iter()
262                    .map(|bin| {
263                        std::str::from_utf8(&bin)
264                            .map(|s| {
265                                if lower {
266                                    s.to_lowercase()
267                                } else {
268                                    s.to_string()
269                                }
270                            })
271                            .unwrap_or_else(|_| general_purpose::URL_SAFE.encode(&bin))
272                    })
273                    .collect();
274                (atype, va)
275            })
276            .collect();
277
278        LdapEntry { dn, attrs }
279    }
280}
281
282#[derive(Debug, Clone)]
283pub struct LdapClientBuilder<U> {
284    url: U,
285    timeout: Duration,
286    /// The maximum LDAP packet size parsed during decoding.
287    max_ber_size: Option<usize>,
288    rustls_client: Option<Arc<ClientConfig>>,
289}
290
291impl<U: Borrow<Url>> LdapClientBuilder<U> {
292    pub fn new(url: U) -> Self {
293        Self {
294            url,
295            timeout: Duration::from_secs(30),
296            max_ber_size: None,
297            rustls_client: None,
298        }
299    }
300
301    pub fn set_tls_config(&mut self, config: Option<Arc<ClientConfig>>) {
302        self.rustls_client = config
303    }
304
305    /// set the rustls [`ClientConfig`] used to handle ldaps connections
306    ///
307    /// if not set uses the platform verifier
308    pub fn with_tls_config(mut self, config: ClientConfig) -> Self {
309        self.set_tls_config(Some(Arc::new(config)));
310        self
311    }
312
313    /// set rustls [`ClientConfig`] to one that does not verify the server certificate
314    pub fn danger_accept_invalid_certs(self) -> Self {
315        warn!("⚠️ CERTIFICATE VERIFICATION IS DISABLED. THIS IS DANGEROUS!!!!");
316        let yolo_cert_validator = Arc::new(YoloCertValidator);
317
318        let client_config = ClientConfig::builder()
319            .dangerous()
320            .with_custom_certificate_verifier(yolo_cert_validator)
321            .with_no_client_auth();
322        self.with_tls_config(client_config)
323    }
324
325    pub fn set_timeout(self, timeout: Duration) -> Self {
326        Self { timeout, ..self }
327    }
328
329    /// Set the maximum size of a decoded message
330    pub fn max_ber_size(self, max_ber_size: Option<usize>) -> Self {
331        Self {
332            max_ber_size,
333            ..self
334        }
335    }
336
337    #[tracing::instrument(level = "debug", skip_all)]
338    pub async fn build(self) -> LdapResult<LdapClient> {
339        let LdapClientBuilder {
340            url,
341            timeout,
342            max_ber_size,
343            rustls_client,
344        } = self;
345
346        let url = url.borrow();
347
348        info!(%url);
349        info!(?timeout);
350
351        // Check the scheme is ldap or ldaps
352        // for now, no ldapi support.
353        let need_tls = match url.scheme() {
354            "ldapi" => return Err(LdapError::LdapiNotSupported),
355            "cldap" => return Err(LdapError::UseCldapTool),
356            "ldap" => false,
357            "ldaps" => true,
358            _ => return Err(LdapError::InvalidUrl),
359        };
360
361        info!(%need_tls);
362        // get domain + port
363
364        // Do we have query params? Can we use them?
365        // https://ldap.com/ldap-urls/
366
367        // resolve to a set of socket addrs.
368        let addrs = url
369            .socket_addrs(|| Some(if need_tls { 636 } else { 389 }))
370            .map_err(|e| {
371                info!(?e, "resolver error");
372                LdapError::ResolverError
373            })?;
374
375        if addrs.is_empty() {
376            return Err(LdapError::ResolverError);
377        }
378
379        addrs.iter().for_each(|address| info!(?address));
380
381        let mut aiter = addrs.into_iter();
382
383        // Try for each to open, with a timeout.
384        let tcpstream = loop {
385            if let Some(addr) = aiter.next() {
386                let sleep = time::sleep(timeout);
387                tokio::pin!(sleep);
388                tokio::select! {
389                    maybe_stream = TcpStream::connect(addr) => {
390                        match maybe_stream {
391                            Ok(t) => {
392                                info!(?addr, "connection established");
393                                break t;
394                            }
395                            Err(e) => {
396                                info!(?addr, ?e, "error");
397                                continue;
398                            }
399                        }
400                    }
401                    _ = &mut sleep => {
402                        info!(?addr, "timeout");
403                        continue;
404                    }
405                }
406            } else {
407                return Err(LdapError::ConnectError);
408            }
409        };
410
411        // If they didn't set it in the builder then set it to the default
412        let max_ber_size = max_ber_size.unwrap_or(ldap3_proto::DEFAULT_MAX_BER_SIZE);
413
414        // If ldaps - start rustls
415        let (write_transport, read_transport) = if need_tls {
416            let tls_client_config = if let Some(client_config) = rustls_client {
417                client_config
418            } else {
419                Arc::new(ClientConfig::with_platform_verifier().map_err(|e| {
420                    error!(?e, "rustls");
421                    LdapError::TlsError
422                })?)
423            };
424
425            let tls_connector = TlsConnector::from(tls_client_config);
426
427            let server_name = match url.host() {
428                Some(Host::Domain(name)) => {
429                    ServerName::try_from(name.to_owned()).map_err(|err| {
430                        error!(?err, "server name invalid");
431                        LdapError::TlsError
432                    })?
433                }
434                Some(Host::Ipv4(addr)) => ServerName::from(addr),
435                Some(Host::Ipv6(addr)) => ServerName::from(addr),
436                None => {
437                    error!("url invalid");
438                    return Err(LdapError::TlsError);
439                }
440            };
441
442            let tlsstream = tls_connector
443                .connect(
444                    server_name,
445                    // Pin::new(&mut tcpstream)
446                    tcpstream,
447                )
448                .await
449                .map_err(|e| {
450                    error!(?e, "rustls");
451                    LdapError::TlsError
452                })?;
453
454            info!("tls configured");
455
456            let (r, w) = tokio::io::split(tlsstream);
457            (
458                LdapWriteTransport::Tls(FramedWrite::new(w, LdapCodec::default())),
459                LdapReadTransport::Tls(FramedRead::new(
460                    r,
461                    LdapCodec::new(Some(max_ber_size), None),
462                )),
463            )
464        } else {
465            let (r, w) = tokio::io::split(tcpstream);
466            (
467                LdapWriteTransport::Plain(FramedWrite::new(w, LdapCodec::default())),
468                LdapReadTransport::Plain(FramedRead::new(
469                    r,
470                    LdapCodec::new(Some(max_ber_size), None),
471                )),
472            )
473        };
474
475        let msg_counter = 1;
476
477        // Good to go - return ok!
478        Ok(LdapClient {
479            read_transport,
480            write_transport,
481            msg_counter,
482        })
483    }
484}
485
486#[derive(Debug)]
487pub struct LdapClient {
488    read_transport: LdapReadTransport,
489    write_transport: LdapWriteTransport,
490    msg_counter: i32,
491}
492
493impl LdapClient {
494    fn get_next_msgid(&mut self) -> i32 {
495        let msgid = self.msg_counter;
496        self.msg_counter += 1;
497        msgid
498    }
499
500    #[tracing::instrument(level = "debug", skip_all)]
501    pub async fn bind<S: Into<String>>(&mut self, dn: S, pw: S) -> LdapResult<()> {
502        let dn = dn.into();
503        info!(%dn);
504        let msgid = self.get_next_msgid();
505
506        let msg = LdapMsg {
507            msgid,
508            op: LdapOp::BindRequest(LdapBindRequest {
509                dn,
510                cred: LdapBindCred::Simple(pw.into()),
511            }),
512            ctrl: vec![],
513        };
514
515        self.write_transport.send(msg).await?;
516
517        // Get the response
518        self.read_transport
519            .next()
520            .await
521            .and_then(|msg| match msg.op {
522                LdapOp::BindResponse(res) => {
523                    if res.res.code == LdapResultCode::Success {
524                        info!("bind success");
525                        Ok(())
526                    } else {
527                        info!(?res.res.code);
528                        Err(LdapError::from(res.res.code))
529                    }
530                }
531                op => {
532                    trace!(?op);
533                    Err(LdapError::InvalidProtocolState)
534                }
535            })
536    }
537
538    #[tracing::instrument(level = "debug", skip_all)]
539    pub async fn whoami(&mut self) -> LdapResult<Option<String>> {
540        let msgid = self.get_next_msgid();
541
542        let msg = LdapMsg {
543            msgid,
544            op: LdapOp::ExtendedRequest(Into::into(LdapWhoamiRequest {})),
545            ctrl: vec![],
546        };
547
548        self.write_transport.send(msg).await?;
549
550        self.read_transport
551            .next()
552            .await
553            .and_then(|msg| match msg.op {
554                LdapOp::ExtendedResponse(ler) => LdapWhoamiResponse::try_from(&ler)
555                    .map_err(|_| LdapError::InvalidProtocolState)
556                    .map(|res| res.dn),
557                op => {
558                    trace!(?op);
559                    Err(LdapError::InvalidProtocolState)
560                }
561            })
562    }
563}
564
565#[derive(Debug)]
566/// This should never be used for anything but testing, as it does no verification!
567struct YoloCertValidator;
568
569impl ServerCertVerifier for YoloCertValidator {
570    fn verify_server_cert(
571        &self,
572        _end_entity: &CertificateDer<'_>,
573        _intermediates: &[CertificateDer<'_>],
574        _server_name: &ServerName<'_>,
575        _ocsp_response: &[u8],
576        _now: UnixTime,
577    ) -> Result<ServerCertVerified, RustlsError> {
578        // Yolo.
579        Ok(ServerCertVerified::assertion())
580    }
581
582    fn verify_tls12_signature(
583        &self,
584        _message: &[u8],
585        _cert: &CertificateDer<'_>,
586        _dss: &DigitallySignedStruct,
587    ) -> Result<HandshakeSignatureValid, RustlsError> {
588        Ok(HandshakeSignatureValid::assertion())
589    }
590
591    fn verify_tls13_signature(
592        &self,
593        _message: &[u8],
594        _cert: &CertificateDer<'_>,
595        _dss: &DigitallySignedStruct,
596    ) -> Result<HandshakeSignatureValid, RustlsError> {
597        Ok(HandshakeSignatureValid::assertion())
598    }
599
600    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
601        vec![
602            SignatureScheme::RSA_PKCS1_SHA384,
603            SignatureScheme::ECDSA_NISTP256_SHA256,
604            SignatureScheme::RSA_PKCS1_SHA256,
605            SignatureScheme::ECDSA_NISTP384_SHA384,
606            SignatureScheme::RSA_PKCS1_SHA512,
607            SignatureScheme::ECDSA_NISTP521_SHA512,
608        ]
609    }
610}
611
612/// Doesn't test the actual *build* step because that requires a live LDAP server.
613#[test]
614fn test_ldapclient_builder() {
615    let url = Url::parse("ldap://ldap.example.com:389").expect("failed to parse URL");
616    let client = LdapClientBuilder::new(&url).max_ber_size(Some(1234567));
617    assert_eq!(client.timeout, Duration::from_secs(30));
618    let client = client.set_timeout(Duration::from_secs(60));
619    assert_eq!(client.timeout, Duration::from_secs(60));
620    assert_eq!(client.max_ber_size, Some(1234567));
621    assert!(client.rustls_client.is_none());
622}