tuwunel_service/users/
ldap.rs1#![cfg(feature = "ldap")]
2
3use std::{collections::HashMap, time::Duration};
4
5use ldap3::{
6 Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry, SearchOptions, dn_escape,
7 ldap_escape,
8};
9use ruma::UserId;
10use tokio::{fs::read as read_file, task::JoinHandle};
11use tuwunel_core::{Result, debug, defer, err, error, implement, result::LogErr, trace};
12
13const CONN_TIMEOUT: Duration = Duration::from_secs(10);
15
16const SEARCH_TIMELIMIT: i32 = 10;
19
20#[implement(super::Service)]
25pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<(String, bool)>> {
26 let localpart = user_id.localpart().to_owned();
27 let lowercased_localpart = localpart.to_lowercase();
28
29 let config = &self.services.config.ldap;
30
31 let (driver, mut ldap) = self.ldap_connect(user_id.as_str()).await?;
32 let abort = driver.abort_handle();
33 defer! {{ abort.abort(); }};
34
35 match (&config.bind_dn, &config.bind_password_file) {
36 | (Some(bind_dn), Some(bind_password_file)) => {
37 let bind_pw = String::from_utf8(read_file(bind_password_file).await?)?;
38
39 ldap.simple_bind(bind_dn, bind_pw.trim())
40 .await
41 .and_then(ldap3::LdapResult::success)
42 .map_err(|e| {
43 error!(%e, "LDAP bind error");
44 err!(Ldap("LDAP bind failed"))
45 })?;
46 },
47 | (..) => {},
48 }
49
50 let attr = [&config.uid_attribute];
51
52 let escaped_localpart = ldap_escape(&lowercased_localpart);
53
54 let user_filter = &config
55 .filter
56 .replace("{username}", &escaped_localpart);
57
58 ldap.with_search_options(SearchOptions::new().timelimit(SEARCH_TIMELIMIT));
59
60 let (entries, _result) = ldap
61 .search(&config.base_dn, Scope::Subtree, user_filter, &attr)
62 .await
63 .and_then(ldap3::SearchResult::success)
64 .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search"))
65 .map_err(|e| {
66 error!(?attr, ?user_filter, %e, "LDAP search error");
67 err!(Ldap("LDAP search failed"))
68 })?;
69
70 let mut dns: HashMap<String, bool> = entries
71 .into_iter()
72 .filter_map(|entry| {
73 let search_entry = SearchEntry::construct(entry);
74 debug!(?search_entry, "LDAP search entry");
75 search_entry
76 .attrs
77 .get(&config.uid_attribute)
78 .is_some_and(|ids| {
79 ids.contains(&localpart) || ids.contains(&lowercased_localpart)
80 })
81 .then_some((search_entry.dn, false))
82 })
83 .collect();
84
85 if !config.admin_filter.is_empty() {
86 let admin_base_dn = if config.admin_base_dn.is_empty() {
87 &config.base_dn
88 } else {
89 &config.admin_base_dn
90 };
91
92 let admin_filter = &config
93 .admin_filter
94 .replace("{username}", &escaped_localpart);
95
96 ldap.with_search_options(SearchOptions::new().timelimit(SEARCH_TIMELIMIT));
97
98 let (admin_entries, _result) = ldap
99 .search(admin_base_dn, Scope::Subtree, admin_filter, &attr)
100 .await
101 .and_then(ldap3::SearchResult::success)
102 .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Admin Search"))
103 .map_err(|e| {
104 error!(?attr, ?admin_filter, %e, "LDAP admin search error");
105 err!(Ldap("LDAP admin search failed"))
106 })?;
107
108 dns.extend(admin_entries.into_iter().filter_map(|entry| {
109 let search_entry = SearchEntry::construct(entry);
110 debug!(?search_entry, "LDAP search entry");
111 search_entry
112 .attrs
113 .get(&config.uid_attribute)
114 .is_some_and(|ids| {
115 ids.contains(&localpart) || ids.contains(&lowercased_localpart)
116 })
117 .then_some((search_entry.dn, true))
118 }));
119 }
120
121 ldap.unbind().await.map_err(|e| {
122 error!(%e, "LDAP unbind error");
123 err!(Ldap("LDAP unbind failed"))
124 })?;
125
126 driver.await.log_err().ok();
127
128 Ok(dns.drain().collect())
129}
130
131#[implement(super::Service)]
132pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result {
133 if password.trim().is_empty() {
135 return Err(err!(Request(Forbidden(debug_error!(
136 "LDAP authentication error: empty password"
137 )))));
138 }
139
140 let (driver, mut ldap) = self.ldap_connect(user_dn).await?;
141 let abort = driver.abort_handle();
142 defer! {{ abort.abort(); }};
143
144 ldap.simple_bind(user_dn, password)
145 .await
146 .and_then(ldap3::LdapResult::success)
147 .map_err(|e| {
148 debug!(%e, "LDAP authentication error");
149 err!(Request(Forbidden("Invalid username or password.")))
150 })?;
151
152 ldap.unbind().await.map_err(|e| {
153 error!(%e, "LDAP unbind error");
154 err!(Ldap("LDAP unbind failed"))
155 })?;
156
157 driver.await.log_err().ok();
158
159 Ok(())
160}
161
162#[implement(super::Service)]
163async fn ldap_connect(&self, principal: &str) -> Result<(JoinHandle<()>, Ldap)> {
164 let uri = self
165 .services
166 .config
167 .ldap
168 .uri
169 .as_ref()
170 .ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?;
171
172 if uri.scheme().starts_with("ldaps") {
173 self.services.globals.init_rustls_provider()?;
174 }
175
176 let settings = LdapConnSettings::new()
177 .set_conn_timeout(CONN_TIMEOUT)
178 .set_no_tls_verify(
179 self.services
180 .config
181 .allow_invalid_tls_certificates,
182 );
183
184 debug!(?uri, "LDAP creating connection...");
185 let (conn, ldap) = LdapConnAsync::from_url_with_settings(settings, uri)
186 .await
187 .map_err(|e| {
188 error!(?principal, %e, "LDAP connection setup error");
189 err!(Ldap("LDAP connection failed"))
190 })?;
191
192 let driver = self.services.server.runtime().spawn(async move {
193 match conn.drive().await {
194 | Err(e) => error!("LDAP connection error: {e}"),
195 | Ok(()) => debug!("LDAP connection completed."),
196 }
197 });
198
199 Ok((driver, ldap))
200}
201
202#[implement(super::Service)]
206#[must_use]
207pub fn ldap_bind_dn(&self, localpart: &str) -> Option<String> {
208 self.services
209 .server
210 .config
211 .ldap
212 .bind_dn
213 .as_ref()
214 .filter(|template| template.contains("{username}"))
215 .map(|template| template.replace("{username}", &dn_escape(localpart)))
216}