|
|
|
@@ -1,21 +1,13 @@
|
|
|
|
|
pub(super) mod dehydrated_device;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
use conduwuit::result::LogErr;
|
|
|
|
|
use conduwuit::{
|
|
|
|
|
Err, Error, Result, Server, debug_warn, err, is_equal_to, trace,
|
|
|
|
|
Err, Error, Result, Server, debug_warn, err, trace,
|
|
|
|
|
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
|
|
|
|
};
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
use conduwuit_core::{debug, error};
|
|
|
|
|
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
|
|
|
|
use futures::{Stream, StreamExt, TryFutureExt};
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
|
|
|
|
|
use ruma::{
|
|
|
|
|
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
|
|
|
|
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
|
|
|
@@ -79,7 +71,6 @@ struct Data {
|
|
|
|
|
userid_displayname: Arc<Map>,
|
|
|
|
|
userid_lastonetimekeyupdate: Arc<Map>,
|
|
|
|
|
userid_masterkeyid: Arc<Map>,
|
|
|
|
|
userid_origin: Arc<Map>,
|
|
|
|
|
userid_password: Arc<Map>,
|
|
|
|
|
userid_suspension: Arc<Map>,
|
|
|
|
|
userid_lock: Arc<Map>,
|
|
|
|
@@ -120,7 +111,6 @@ impl crate::Service for Service {
|
|
|
|
|
userid_displayname: args.db["userid_displayname"].clone(),
|
|
|
|
|
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
|
|
|
|
|
userid_masterkeyid: args.db["userid_masterkeyid"].clone(),
|
|
|
|
|
userid_origin: args.db["userid_origin"].clone(),
|
|
|
|
|
userid_password: args.db["userid_password"].clone(),
|
|
|
|
|
userid_suspension: args.db["userid_suspension"].clone(),
|
|
|
|
|
userid_lock: args.db["userid_lock"].clone(),
|
|
|
|
@@ -178,26 +168,12 @@ impl Service {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a new user account on this homeserver.
|
|
|
|
|
///
|
|
|
|
|
/// User origin is by default "password" (meaning that it will login using
|
|
|
|
|
/// its user_id/password). Users with other origins (currently only "ldap"
|
|
|
|
|
/// is available) have special login processes.
|
|
|
|
|
#[inline]
|
|
|
|
|
pub async fn create(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: &UserId,
|
|
|
|
|
password: Option<&str>,
|
|
|
|
|
origin: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
if !self.services.globals.user_is_local(user_id)
|
|
|
|
|
&& (password.is_some() || origin.is_some())
|
|
|
|
|
{
|
|
|
|
|
return Err!("Cannot create a nonlocal user with a set password or origin");
|
|
|
|
|
pub async fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
|
|
|
|
if !self.services.globals.user_is_local(user_id) && password.is_some() {
|
|
|
|
|
return Err!("Cannot create a nonlocal user with a set password");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.db
|
|
|
|
|
.userid_origin
|
|
|
|
|
.insert(user_id, origin.unwrap_or("password"));
|
|
|
|
|
self.set_password(user_id, password).await?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
@@ -360,11 +336,6 @@ impl Service {
|
|
|
|
|
.ready_filter_map(|(u, p): (OwnedUserId, &[u8])| (!p.is_empty()).then_some(u))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the origin of the user (password/LDAP/...).
|
|
|
|
|
pub async fn origin(&self, user_id: &UserId) -> Result<String> {
|
|
|
|
|
self.db.userid_origin.get(user_id).await.deserialized()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the password hash for the given user.
|
|
|
|
|
pub async fn password_hash(&self, user_id: &UserId) -> Result<String> {
|
|
|
|
|
self.db.userid_password.get(user_id).await.deserialized()
|
|
|
|
@@ -372,22 +343,6 @@ impl Service {
|
|
|
|
|
|
|
|
|
|
/// Hash and set the user's password to the Argon2 hash
|
|
|
|
|
pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
|
|
|
|
// Cannot change the password of a LDAP user. There are two special cases :
|
|
|
|
|
// - a `None` password can be used to deactivate a LDAP user
|
|
|
|
|
// - a "*" password is used as the default password of an active LDAP user
|
|
|
|
|
if cfg!(feature = "ldap")
|
|
|
|
|
&& password.is_some_and(|pwd| pwd != "*")
|
|
|
|
|
&& self
|
|
|
|
|
.db
|
|
|
|
|
.userid_origin
|
|
|
|
|
.get(user_id)
|
|
|
|
|
.await
|
|
|
|
|
.deserialized::<String>()
|
|
|
|
|
.is_ok_and(is_equal_to!("ldap"))
|
|
|
|
|
{
|
|
|
|
|
return Err!(Request(InvalidParam("Cannot change password of a LDAP user")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
password
|
|
|
|
|
.map(utils::hash::password)
|
|
|
|
|
.transpose()
|
|
|
|
@@ -1292,177 +1247,6 @@ impl Service {
|
|
|
|
|
.ready_for_each(|(key, _)| self.set_profile_key(user_id, &key, None))
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
async fn create_ldap_connection(
|
|
|
|
|
config: &conduwuit_core::config::LdapConfig,
|
|
|
|
|
uri: &str,
|
|
|
|
|
) -> Result<(LdapConnAsync, ldap3::Ldap), ldap3::LdapError> {
|
|
|
|
|
let mut settings = LdapConnSettings::new();
|
|
|
|
|
|
|
|
|
|
if config.use_starttls {
|
|
|
|
|
settings = settings.set_starttls(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config.disable_tls_verification {
|
|
|
|
|
settings = settings.set_no_tls_verify(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LdapConnAsync::with_settings(settings, uri).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "ldap"))]
|
|
|
|
|
pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, Option<bool>)>> {
|
|
|
|
|
Err!(FeatureDisabled("ldap"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<(String, Option<bool>)>> {
|
|
|
|
|
let localpart = user_id.localpart().to_owned();
|
|
|
|
|
let lowercased_localpart = localpart.to_lowercase();
|
|
|
|
|
|
|
|
|
|
let config = &self.services.server.config.ldap;
|
|
|
|
|
let uri = config
|
|
|
|
|
.uri
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?;
|
|
|
|
|
|
|
|
|
|
debug!(?uri, "LDAP creating connection...");
|
|
|
|
|
let (conn, mut ldap) = Self::create_ldap_connection(config, uri.as_str())
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| err!(Ldap(error!(%user_id, "LDAP connection setup error: {e}"))))?;
|
|
|
|
|
|
|
|
|
|
let driver = self.services.server.runtime().spawn(async move {
|
|
|
|
|
match conn.drive().await {
|
|
|
|
|
| Err(e) => error!("LDAP connection error: {e}"),
|
|
|
|
|
| Ok(()) => debug!("LDAP connection completed."),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
match (&config.bind_dn, &config.bind_password_file) {
|
|
|
|
|
| (Some(bind_dn), Some(bind_password_file)) => {
|
|
|
|
|
let bind_pw = String::from_utf8(std::fs::read(bind_password_file)?)?;
|
|
|
|
|
ldap.simple_bind(bind_dn, bind_pw.trim())
|
|
|
|
|
.await
|
|
|
|
|
.and_then(ldap3::LdapResult::success)
|
|
|
|
|
.map_err(|e| err!(Ldap(error!("LDAP bind error: {e}"))))?;
|
|
|
|
|
},
|
|
|
|
|
| (..) => {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attr = [&config.uid_attribute, &config.name_attribute];
|
|
|
|
|
|
|
|
|
|
let user_filter = &config.filter.replace("{username}", &lowercased_localpart);
|
|
|
|
|
|
|
|
|
|
let (entries, _result) = ldap
|
|
|
|
|
.search(&config.base_dn, Scope::Subtree, user_filter, &attr)
|
|
|
|
|
.await
|
|
|
|
|
.and_then(ldap3::SearchResult::success)
|
|
|
|
|
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search"))
|
|
|
|
|
.map_err(|e| err!(Ldap(error!(?attr, ?user_filter, "LDAP search error: {e}"))))?;
|
|
|
|
|
|
|
|
|
|
let mut dns: HashMap<String, Option<bool>> = entries
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|entry| {
|
|
|
|
|
let search_entry = SearchEntry::construct(entry);
|
|
|
|
|
debug!(?search_entry, "LDAP search entry");
|
|
|
|
|
search_entry
|
|
|
|
|
.attrs
|
|
|
|
|
.get(&config.uid_attribute)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.chain(search_entry.attrs.get(&config.name_attribute))
|
|
|
|
|
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
|
|
|
|
|
.then_some((search_entry.dn, None))
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if !config.admin_filter.is_empty() {
|
|
|
|
|
// Update all existing entries to Some(false) since we can now determine admin
|
|
|
|
|
// status
|
|
|
|
|
for admin_status in dns.values_mut() {
|
|
|
|
|
*admin_status = Some(false);
|
|
|
|
|
}
|
|
|
|
|
let admin_base_dn = if config.admin_base_dn.is_empty() {
|
|
|
|
|
&config.base_dn
|
|
|
|
|
} else {
|
|
|
|
|
&config.admin_base_dn
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let admin_filter = &config
|
|
|
|
|
.admin_filter
|
|
|
|
|
.replace("{username}", &lowercased_localpart);
|
|
|
|
|
|
|
|
|
|
let (admin_entries, _result) = ldap
|
|
|
|
|
.search(admin_base_dn, Scope::Subtree, admin_filter, &attr)
|
|
|
|
|
.await
|
|
|
|
|
.and_then(ldap3::SearchResult::success)
|
|
|
|
|
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Admin Search"))
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
err!(Ldap(error!(?attr, ?admin_filter, "Ldap admin search error: {e}")))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
dns.extend(admin_entries.into_iter().filter_map(|entry| {
|
|
|
|
|
let search_entry = SearchEntry::construct(entry);
|
|
|
|
|
debug!(?search_entry, "LDAP search entry");
|
|
|
|
|
search_entry
|
|
|
|
|
.attrs
|
|
|
|
|
.get(&config.uid_attribute)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.chain(search_entry.attrs.get(&config.name_attribute))
|
|
|
|
|
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
|
|
|
|
|
.then_some((search_entry.dn, Some(true)))
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ldap.unbind()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?;
|
|
|
|
|
|
|
|
|
|
driver.await.log_err().ok();
|
|
|
|
|
|
|
|
|
|
Ok(dns.drain().collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "ldap"))]
|
|
|
|
|
pub async fn auth_ldap(&self, _user_dn: &str, _password: &str) -> Result {
|
|
|
|
|
Err!(FeatureDisabled("ldap"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "ldap")]
|
|
|
|
|
pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result {
|
|
|
|
|
let config = &self.services.server.config.ldap;
|
|
|
|
|
let uri = config
|
|
|
|
|
.uri
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?;
|
|
|
|
|
|
|
|
|
|
debug!(?uri, "LDAP creating connection...");
|
|
|
|
|
let (conn, mut ldap) = Self::create_ldap_connection(config, uri.as_str())
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| err!(Ldap(error!(%user_dn, "LDAP connection setup error: {e}"))))?;
|
|
|
|
|
|
|
|
|
|
let driver = self.services.server.runtime().spawn(async move {
|
|
|
|
|
match conn.drive().await {
|
|
|
|
|
| Err(e) => error!("LDAP connection error: {e}"),
|
|
|
|
|
| Ok(()) => debug!("LDAP connection completed."),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ldap.simple_bind(user_dn, password)
|
|
|
|
|
.await
|
|
|
|
|
.and_then(ldap3::LdapResult::success)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
err!(Request(Forbidden(debug_error!("LDAP authentication error: {e}"))))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
ldap.unbind()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?;
|
|
|
|
|
|
|
|
|
|
driver.await.log_err().ok();
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn parse_master_key(
|
|
|
|
|