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