rustlavel_ldap/lib.rs
1//! rustlavel-ldap: authenticate a person against a directory.
2//!
3//! LDAP is how most organisations answer "who is this and what is their
4//! password", and an application that needs that answer should not need to
5//! learn ASN.1 to get it. So the package has two faces: [`directory`], which is
6//! one call, and [`protocol`] and [`ber`] underneath it for anything that call
7//! does not cover.
8//!
9//! ```ignore
10//! use rustlavel_ldap::prelude::*;
11//!
12//! let directory = Directory::new("ldaps://dc.example.test")?
13//! .service_account("cn=svc,dc=example,dc=test", &service_password)
14//! .base_dn("ou=people,dc=example,dc=test")
15//! .username_attribute("uid")
16//! .filter(Filter::equals("objectClass", "inetOrgPerson"))
17//! .attributes(["cn", "mail"]);
18//!
19//! let user = directory.authenticate(&username, &password).await?;
20//! println!("{} is {}", user.dn(), user.value("cn").unwrap_or("unnamed"));
21//! ```
22//!
23//! # The protocol is written here
24//!
25//! LDAP v3 (RFC 4511) is ASN.1 BER over TCP. BER is a serialisation format
26//! rather than cryptography, so rule one applies and it is written in [`ber`]:
27//! a small, strict subset that refuses everything RFC 4511 says a directory may
28//! not send. TLS is the exception, and comes from rustls with the `aws_lc_rs`
29//! provider and `prefer-post-quantum`, like every other TLS in this framework.
30//!
31//! # Three things this package is firm about
32//!
33//! **A simple bind sends the password in the clear.** That is what a simple
34//! bind *is* — RFC 4511 §4.2 puts the password in an octet string with nothing
35//! around it. So one is refused over an unencrypted connection unless the
36//! caller has said, in as many words, that it is acceptable
37//! ([`LdapConfig::allow_plaintext_password`]). This is the most common LDAP
38//! deployment mistake there is, and it is invisible from the outside because
39//! everything works.
40//!
41//! **An empty password must not authenticate.** LDAP treats a simple bind with
42//! a zero-length password as an *anonymous* bind, which succeeds — so a login
43//! form with a blank password field would report a successful login for any
44//! username that exists. This has been a real vulnerability in real products
45//! more than once. It is refused in [`Operation::simple_bind`], where the bytes
46//! are built, and again in [`Directory::authenticate`] before any I/O happens.
47//!
48//! **A username is a value, not text.** The classic LDAP injection is a filter
49//! template with a name pasted into it: a username of `*` turns a lookup into a
50//! wildcard that matches every account. [`Filter`] is a type, and
51//! [`Filter::encode`] writes each value as a length-delimited BER octet string
52//! where `*` and `)` have no syntax at all. [`escape_filter_value`] exists for
53//! the cases that genuinely need the RFC 4515 string form.
54
55pub mod ber;
56pub mod connection;
57pub mod directory;
58pub mod protocol;
59
60pub use connection::{Encryption, LdapConfig, LdapConnection};
61pub use directory::{AccountProblem, AuthenticationError, Directory, User};
62pub use protocol::{
63 Attribute, Control, DerefAliases, Filter, LdapMessage, LdapResult, Operation, ProtocolOp,
64 ResultCode, Scope, SearchEntry, SearchOutcome, SearchRequest, escape_dn_value,
65 escape_filter_value,
66};
67
68pub use rustlavel_core::{Error, Result};
69
70/// What an application importing this package normally wants.
71pub mod prelude {
72 pub use crate::connection::{Encryption, LdapConfig, LdapConnection};
73 pub use crate::directory::{AccountProblem, AuthenticationError, Directory, User};
74 pub use crate::protocol::{
75 Filter, ResultCode, Scope, SearchEntry, SearchRequest, escape_dn_value,
76 escape_filter_value,
77 };
78}