3 fmt
::{Display, Formatter}
,
9 use anyhow
::{bail, format_err, Context, Error}
;
10 use ldap3
::adapters
::{Adapter, EntriesOnly, PagedResults}
;
11 use ldap3
::{Ldap, LdapConnAsync, LdapConnSettings, LdapResult, Scope, SearchEntry}
;
12 use native_tls
::{Certificate, TlsConnector, TlsConnectorBuilder}
;
13 use serde
::{Deserialize, Serialize}
;
15 #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
16 /// LDAP connection security
17 pub enum ConnectionMode
{
18 /// unencrypted connection
20 /// upgrade to TLS via STARTTLS
26 #[derive(Clone, Serialize, Deserialize)]
27 /// Configuration for LDAP connections
29 /// Array of servers that will be tried in order
30 pub servers
: Vec
<String
>,
32 pub port
: Option
<u16>,
33 /// LDAP attribute containing the user id. Will be used to look up the user's domain
34 pub user_attr
: String
,
37 /// LDAP bind domain, will be used for user lookup/sync if set
38 pub bind_dn
: Option
<String
>,
39 /// LDAP bind password, will be used for user lookup/sync if set
40 pub bind_password
: Option
<String
>,
41 /// Connection security
42 pub tls_mode
: ConnectionMode
,
43 /// Verify the server's TLS certificate
44 pub verify_certificate
: bool
,
45 /// Root certificates that should be trusted, in addition to
46 /// the ones from the certificate store.
47 /// Expects X.509 certs in PEM format.
48 pub additional_trusted_certificates
: Option
<Vec
<PathBuf
>>,
49 /// Override the path to the system's default certificate store
50 /// in /etc/ssl/certs (added for PVE compatibility)
51 pub certificate_store_path
: Option
<PathBuf
>,
54 #[derive(Serialize, Deserialize)]
55 /// Parameters for LDAP user searches
56 pub struct SearchParameters
{
57 /// Attributes that should be retrieved
58 pub attributes
: Vec
<String
>,
59 /// `objectclass`es of intereset
60 pub user_classes
: Vec
<String
>,
61 /// Custom user filter
62 pub user_filter
: Option
<String
>,
65 #[derive(Serialize, Deserialize, Debug)]
66 /// Single LDAP user search result
67 pub struct SearchResult
{
68 /// The full user's domain
70 /// Queried user attributes
71 pub attributes
: HashMap
<String
, Vec
<String
>>,
74 /// Connection to an LDAP server, can be used to authenticate users.
75 pub struct Connection
{
76 /// Configuration for this connection
81 /// Default port for LDAP/StartTls connections
82 const LDAP_DEFAULT_PORT
: u16 = 389;
83 /// Default port for LDAPS connections
84 const LDAPS_DEFAULT_PORT
: u16 = 636;
85 /// Connection timeout
86 const LDAP_CONNECTION_TIMEOUT
: Duration
= Duration
::from_secs(5);
88 /// Create a new LDAP connection.
89 pub fn new(config
: Config
) -> Self {
93 /// Authenticate a user with username/password.
95 /// The user's domain is queried is by performing an LDAP search with the configured bind_dn
96 /// and bind_password. If no bind_dn is provided, an anonymous search is attempted.
97 pub async
fn authenticate_user(&self, username
: &str, password
: &str) -> Result
<(), Error
> {
98 let user_dn
= self.search_user_dn(username
).await?
;
100 let mut ldap
= self.create_connection().await?
;
102 // Perform actual user authentication by binding.
103 let _
: LdapResult
= ldap
.simple_bind(&user_dn
, password
).await?
.success()?
;
105 // We are already authenticated, so don't fail if terminating the connection
106 // does not work for some reason.
107 let _
: Result
<(), _
> = ldap
.unbind().await
;
112 /// Query entities matching given search parameters
113 pub async
fn search_entities(
115 parameters
: &SearchParameters
,
116 ) -> Result
<Vec
<SearchResult
>, Error
> {
117 let search_filter
= Self::assemble_search_filter(parameters
);
119 let mut ldap
= self.create_connection().await?
;
121 if let Some(bind_dn
) = self.config
.bind_dn
.as_deref() {
126 .ok_or_else(|| format_err
!("Missing bind password for {bind_dn}"))?
;
127 let _
: LdapResult
= ldap
.simple_bind(bind_dn
, password
).await?
.success()?
;
130 let adapters
: Vec
<Box
<dyn Adapter
<_
, _
>>> = vec
![
131 Box
::new(EntriesOnly
::new()),
132 Box
::new(PagedResults
::new(500)),
134 let mut search
= ldap
135 .streaming_search_with(
137 &self.config
.base_dn
,
140 parameters
.attributes
.clone(),
144 let mut results
= Vec
::new();
146 while let Some(entry
) = search
.next().await?
{
147 let entry
= SearchEntry
::construct(entry
);
149 results
.push(SearchResult
{
151 attributes
: entry
.attrs
,
154 let _res
= search
.finish().await
.success()?
;
156 let _
= ldap
.unbind().await
;
161 /// Helper to check if a connection with the current configuration is possible.
163 /// This performs a search with the current configuration. If the search succeeds `Ok(()) is
164 /// returned, otherwise an `Error` is returned.
165 pub async
fn check_connection(&self) -> Result
<(), Error
> {
166 let mut ldap
= self.create_connection().await?
;
168 if let Some(bind_dn
) = self.config
.bind_dn
.as_deref() {
173 .ok_or_else(|| format_err
!("Missing bind password for {bind_dn}"))?
;
175 let _
: LdapResult
= ldap
176 .simple_bind(bind_dn
, password
)
179 .context("LDAP bind failed, bind_dn or password could be incorrect")?
;
182 // only search base to make sure the base_dn exists while avoiding most common size limits
185 &self.config
.base_dn
,
192 .context("Could not search LDAP realm, base_dn could be incorrect")?
;
194 if self.config
.bind_dn
.is_some() {
195 let _
: Result
<(), _
> = ldap
.unbind().await
; // ignore errors, search succeeded already
201 /// Retrive port from LDAP configuration, otherwise use the correct default
202 fn port_from_config(&self) -> u16 {
203 self.config
.port
.unwrap_or_else(|| {
204 if self.config
.tls_mode
== ConnectionMode
::Ldaps
{
205 Self::LDAPS_DEFAULT_PORT
207 Self::LDAP_DEFAULT_PORT
212 /// Determine correct URL scheme from LDAP config
213 fn scheme_from_config(&self) -> &'
static str {
214 if self.config
.tls_mode
== ConnectionMode
::Ldaps
{
221 /// Construct URL from LDAP config
222 fn ldap_url_from_config(&self, server
: &str) -> String
{
223 let port
= self.port_from_config();
224 let scheme
= self.scheme_from_config();
225 format
!("{scheme}://{server}:{port}")
228 fn add_cert_to_builder
<P
: AsRef
<Path
>>(
230 builder
: &mut TlsConnectorBuilder
,
231 ) -> Result
<(), Error
> {
232 let bytes
= fs
::read(path
)?
;
233 let cert
= Certificate
::from_pem(&bytes
)?
;
234 builder
.add_root_certificate(cert
);
239 async
fn try_connect(&self, url
: &str) -> Result
<(LdapConnAsync
, Ldap
), Error
> {
240 let starttls
= self.config
.tls_mode
== ConnectionMode
::StartTls
;
242 let mut builder
= TlsConnector
::builder();
243 builder
.danger_accept_invalid_certs(!self.config
.verify_certificate
);
245 if let Some(certificate_paths
) = self.config
.additional_trusted_certificates
.as_deref() {
246 for path
in certificate_paths
{
247 Self::add_cert_to_builder(path
, &mut builder
)?
;
251 if let Some(certificate_store_path
) = self.config
.certificate_store_path
.as_deref() {
252 builder
.disable_built_in_roots(true);
254 for dir_entry
in fs
::read_dir(certificate_store_path
)?
{
255 let dir_entry
= dir_entry?
;
257 if !dir_entry
.metadata()?
.is_dir() {
258 Self::add_cert_to_builder(dir_entry
.path(), &mut builder
)?
;
263 LdapConnAsync
::with_settings(
264 LdapConnSettings
::new()
265 .set_starttls(starttls
)
266 .set_conn_timeout(Self::LDAP_CONNECTION_TIMEOUT
)
267 .set_connector(builder
.build()?
),
271 .map_err(|e
| e
.into())
274 /// Create LDAP connection
276 /// If a connection to the server cannot be established, the fallbacks
278 async
fn create_connection(&self) -> Result
<Ldap
, Error
> {
279 let mut last_error
= None
;
281 for server
in &self.config
.servers
{
282 match self.try_connect(&self.ldap_url_from_config(server
)).await
{
283 Ok((connection
, ldap
)) => {
284 ldap3
::drive
!(connection
);
288 last_error
= Some(e
);
293 Err(last_error
.unwrap())
296 /// Search a user's domain.
297 async
fn search_user_dn(&self, username
: &str) -> Result
<String
, Error
> {
298 let mut ldap
= self.create_connection().await?
;
300 if let Some(bind_dn
) = self.config
.bind_dn
.as_deref() {
305 .ok_or_else(|| format_err
!("Missing bind password for {bind_dn}"))?
;
306 let _
: LdapResult
= ldap
.simple_bind(bind_dn
, password
).await?
.success()?
;
308 let user_dn
= self.do_search_user_dn(username
, &mut ldap
).await
;
310 ldap
.unbind().await?
;
314 self.do_search_user_dn(username
, &mut ldap
).await
318 async
fn do_search_user_dn(&self, username
: &str, ldap
: &mut Ldap
) -> Result
<String
, Error
> {
319 let query
= format
!("(&({}={}))", self.config
.user_attr
, username
);
321 let (entries
, _res
) = ldap
322 .search(&self.config
.base_dn
, Scope
::Subtree
, &query
, vec
!["dn"])
326 if entries
.len() > 1 {
328 "found multiple users with attribute `{}={}`",
329 self.config
.user_attr
,
334 if let Some(entry
) = entries
.into_iter().next() {
335 let entry
= SearchEntry
::construct(entry
);
340 bail
!("user not found")
343 fn assemble_search_filter(parameters
: &SearchParameters
) -> String
{
344 use FilterElement
::*;
346 let attr_wildcards
= Or(parameters
349 .map(|attr
| Condition(attr
, "*"))
351 let user_classes
= Or(parameters
354 .map(|class
| Condition("objectclass", class
))
357 if let Some(user_filter
) = ¶meters
.user_filter
{
358 And(vec
![Verbatim(user_filter
), attr_wildcards
, user_classes
])
360 And(vec
![attr_wildcards
, user_classes
])
367 enum FilterElement
<'a
> {
368 And(Vec
<FilterElement
<'a
>>),
369 Or(Vec
<FilterElement
<'a
>>),
370 Condition(&'a
str, &'a
str),
371 Not(Box
<FilterElement
<'a
>>),
375 impl<'a
> Display
for FilterElement
<'a
> {
376 fn fmt(&self, f
: &mut Formatter
<'_
>) -> std
::fmt
::Result
{
377 fn write_children(f
: &mut Formatter
<'_
>, children
: &[FilterElement
]) -> std
::fmt
::Result
{
378 for child
in children
{
379 write
!(f
, "{child}")?
;
386 FilterElement
::And(children
) => {
388 write_children(f
, children
)?
;
391 FilterElement
::Or(children
) => {
393 write_children(f
, children
)?
;
396 FilterElement
::Not(element
) => {
397 write
!(f
, "(!{element})")?
;
399 FilterElement
::Condition(attr
, value
) => {
400 write
!(f
, "({attr}={value})")?
;
402 FilterElement
::Verbatim(verbatim
) => {
403 if !verbatim
.starts_with('
('
) && !verbatim
.ends_with('
)'
) {
404 write
!(f
, "({verbatim})")?
406 write
!(f
, "{verbatim}")?
417 use super::FilterElement
::*;
420 fn test_filter_elements_to_string() {
421 assert_eq
!("(uid=john)", Condition("uid", "john").to_string());
424 Not(Box
::new(Condition("uid", "john"))).to_string()
427 assert_eq
!("(foo=bar)", &Verbatim("(foo=bar)").to_string());
428 assert_eq
!("(foo=bar)", &Verbatim("foo=bar").to_string());
430 let filter_string
= And(vec
![
431 Condition("givenname", "john"),
432 Condition("sn", "doe"),
434 Condition("email", "john@foo"),
435 Condition("email", "john@bar"),
441 "(&(givenname=john)(sn=doe)(|(email=john@foo)(email=john@bar)))",