feat: add ldap config
feat: add LDAP login and user creation feat: add diagnostic commands Co-authored-by: Jason Volk <jason@zemos.net> Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
489
Cargo.lock
generated
489
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -549,6 +549,11 @@ features = ["serde"]
|
|||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
|
[workspace.dependencies.ldap3]
|
||||||
|
version = "0.11"
|
||||||
|
default-features = false
|
||||||
|
features = ["sync", "tls-rustls"]
|
||||||
|
|
||||||
#
|
#
|
||||||
# Patches
|
# Patches
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -94,6 +94,39 @@ pub(crate) enum UsersCommand {
|
|||||||
user_a: OwnedUserId,
|
user_a: OwnedUserId,
|
||||||
user_b: OwnedUserId,
|
user_b: OwnedUserId,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
SearchLdap {
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
},
|
||||||
|
|
||||||
|
AuthLdap {
|
||||||
|
user_dn: String,
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn auth_ldap(&self, user_dn: String, password: String) -> Result {
|
||||||
|
let timer = tokio::time::Instant::now();
|
||||||
|
let result = self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.auth_ldap(&user_dn, &password)
|
||||||
|
.await;
|
||||||
|
let query_time = timer.elapsed();
|
||||||
|
|
||||||
|
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn search_ldap(&self, user_id: OwnedUserId) -> Result {
|
||||||
|
let timer = tokio::time::Instant::now();
|
||||||
|
let result = self.services.users.search_ldap(&user_id).await;
|
||||||
|
let query_time = timer.elapsed();
|
||||||
|
|
||||||
|
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
|
|||||||
@@ -372,7 +372,10 @@ pub(crate) async fn register_route(
|
|||||||
let password = if is_guest { None } else { body.password.as_deref() };
|
let password = if is_guest { None } else { body.password.as_deref() };
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
services.users.create(&user_id, password, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&user_id, password, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Default to pretty displayname
|
// Default to pretty displayname
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
let mut displayname = user_id.localpart().to_owned();
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ pub(crate) async fn get_displayname_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !services.users.exists(&body.user_id).await {
|
if !services.users.exists(&body.user_id).await {
|
||||||
services.users.create(&body.user_id, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&body.user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
@@ -193,7 +196,10 @@ pub(crate) async fn get_avatar_url_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !services.users.exists(&body.user_id).await {
|
if !services.users.exists(&body.user_id).await {
|
||||||
services.users.create(&body.user_id, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&body.user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
@@ -255,7 +261,10 @@ pub(crate) async fn get_profile_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !services.users.exists(&body.user_id).await {
|
if !services.users.exists(&body.user_id).await {
|
||||||
services.users.create(&body.user_id, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&body.user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::StreamExt;
|
use futures::{StreamExt, TryFutureExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
UserId,
|
OwnedUserId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
session::{
|
session::{
|
||||||
get_login_token,
|
get_login_token,
|
||||||
@@ -22,10 +22,10 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, debug, err, info, utils,
|
Err, Error, Result, debug, debug_error, err, info, utils,
|
||||||
utils::{ReadyExt, hash},
|
utils::{hash, stream::ReadyExt},
|
||||||
};
|
};
|
||||||
use tuwunel_service::uiaa::SESSION_ID_LENGTH;
|
use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -49,6 +49,82 @@ pub(crate) async fn get_login_types_route(
|
|||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authenticates the given user by its ID and its password.
|
||||||
|
///
|
||||||
|
/// Returns the user ID if successful, and an error otherwise.
|
||||||
|
async fn password_login(
|
||||||
|
services: &Services,
|
||||||
|
user_id: &UserId,
|
||||||
|
lowercased_user_id: &UserId,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<OwnedUserId> {
|
||||||
|
let (hash, user_id) = services
|
||||||
|
.users
|
||||||
|
.password_hash(user_id)
|
||||||
|
.map_ok(|hash| (hash, user_id))
|
||||||
|
.or_else(|_| {
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.password_hash(lowercased_user_id)
|
||||||
|
.map_ok(|hash| (hash, lowercased_user_id))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if hash.is_empty() {
|
||||||
|
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
||||||
|
}
|
||||||
|
|
||||||
|
hash::verify_password(password, &hash)
|
||||||
|
.inspect_err(|e| debug_error!("{e}"))
|
||||||
|
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||||
|
|
||||||
|
Ok(user_id.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticates the given user through the configured LDAP server.
|
||||||
|
///
|
||||||
|
/// Creates the user if the user is found in the LDAP and do not already have an
|
||||||
|
/// account.
|
||||||
|
async fn ldap_login(
|
||||||
|
services: &Services,
|
||||||
|
user_id: &UserId,
|
||||||
|
lowercased_user_id: &UserId,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<OwnedUserId> {
|
||||||
|
debug!("Searching user in LDAP");
|
||||||
|
|
||||||
|
let dns = services.users.search_ldap(user_id).await?;
|
||||||
|
|
||||||
|
if dns.len() >= 2 {
|
||||||
|
return Err!(Ldap("LDAP search returned two or more results"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(user_dn) = dns.first() else {
|
||||||
|
return password_login(services, user_id, lowercased_user_id, password).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
// LDAP users are automatically created on first login attempt. This is a very
|
||||||
|
// common feature that can be seen on many services using a LDAP provider for
|
||||||
|
// their users (synapse, Nextcloud, Jellyfin, ...).
|
||||||
|
//
|
||||||
|
// LDAP users are crated with a dummy password but non empty because an empty
|
||||||
|
// password is reserved for deactivated accounts. The tuwunel password field
|
||||||
|
// will never be read to login a LDAP user so it's not an issue.
|
||||||
|
if !services.users.exists(lowercased_user_id).await {
|
||||||
|
debug!("Creating user {lowercased_user_id} from LDAP");
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.create(lowercased_user_id, Some("*"), Some("ldap"))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.auth_ldap(user_dn, password)
|
||||||
|
.await
|
||||||
|
.map(|()| lowercased_user_id.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/login`
|
/// # `POST /_matrix/client/v3/login`
|
||||||
///
|
///
|
||||||
/// Authenticates the user and returns an access token it can use in subsequent
|
/// Authenticates the user and returns an access token it can use in subsequent
|
||||||
@@ -107,43 +183,10 @@ pub(crate) async fn login_route(
|
|||||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// first try the username as-is
|
if services.config.ldap.enable {
|
||||||
let hash = services
|
ldap_login(&services, &user_id, &lowercased_user_id, password).await?
|
||||||
.users
|
} else {
|
||||||
.password_hash(&user_id)
|
password_login(&services, &user_id, &lowercased_user_id, password).await?
|
||||||
.await
|
|
||||||
.inspect_err(|e| debug!("{e}"));
|
|
||||||
|
|
||||||
match hash {
|
|
||||||
| Ok(hash) => {
|
|
||||||
if hash.is_empty() {
|
|
||||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
|
||||||
}
|
|
||||||
|
|
||||||
hash::verify_password(password, &hash)
|
|
||||||
.inspect_err(|e| debug!("{e}"))
|
|
||||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
|
||||||
|
|
||||||
user_id
|
|
||||||
},
|
|
||||||
| Err(_e) => {
|
|
||||||
let hash_lowercased_user_id = services
|
|
||||||
.users
|
|
||||||
.password_hash(&lowercased_user_id)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| debug!("{e}"))
|
|
||||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
|
||||||
|
|
||||||
if hash_lowercased_user_id.is_empty() {
|
|
||||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
|
||||||
}
|
|
||||||
|
|
||||||
hash::verify_password(password, &hash_lowercased_user_id)
|
|
||||||
.inspect_err(|e| debug!("{e}"))
|
|
||||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
|
||||||
|
|
||||||
lowercased_user_id
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||||
|
|||||||
@@ -306,7 +306,10 @@ pub(crate) async fn get_timezone_key_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !services.users.exists(&body.user_id).await {
|
if !services.users.exists(&body.user_id).await {
|
||||||
services.users.create(&body.user_id, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&body.user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
@@ -366,7 +369,10 @@ pub(crate) async fn get_profile_key_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !services.users.exists(&body.user_id).await {
|
if !services.users.exists(&body.user_id).await {
|
||||||
services.users.create(&body.user_id, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(&body.user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ http-body-util.workspace = true
|
|||||||
http.workspace = true
|
http.workspace = true
|
||||||
ipaddress.workspace = true
|
ipaddress.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
ldap3.workspace = true
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
libloading.workspace = true
|
libloading.workspace = true
|
||||||
libloading.optional = true
|
libloading.optional = true
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ use crate::{Result, err, error::Error, utils::sys};
|
|||||||
### For more information, see:
|
### For more information, see:
|
||||||
### https://tuwunel.chat/configuration.html
|
### https://tuwunel.chat/configuration.html
|
||||||
"#,
|
"#,
|
||||||
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates"
|
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap"
|
||||||
)]
|
)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// The server_name is the pretty name of this server. It is used as a
|
/// The server_name is the pretty name of this server. It is used as a
|
||||||
@@ -1804,6 +1804,17 @@ pub struct Config {
|
|||||||
// external structure; separate section
|
// external structure; separate section
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub blurhashing: BlurhashConfig,
|
pub blurhashing: BlurhashConfig,
|
||||||
|
|
||||||
|
#[cfg(not(doctest))]
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - No LDAP login (default):
|
||||||
|
///
|
||||||
|
/// ldap = "none"
|
||||||
|
///
|
||||||
|
/// default: "none"
|
||||||
|
pub ldap: LdapConfig,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
#[allow(clippy::zero_sized_map_values)]
|
#[allow(clippy::zero_sized_map_values)]
|
||||||
// this is a catchall, the map shouldn't be zero at runtime
|
// this is a catchall, the map shouldn't be zero at runtime
|
||||||
@@ -1885,6 +1896,75 @@ pub struct BlurhashConfig {
|
|||||||
pub blurhash_max_raw_size: u64,
|
pub blurhash_max_raw_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[config_example_generator(filename = "tuwunel-example.toml", section = "global.ldap")]
|
||||||
|
pub struct LdapConfig {
|
||||||
|
/// Whether to enable LDAP login.
|
||||||
|
///
|
||||||
|
/// example: "true"
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable: bool,
|
||||||
|
|
||||||
|
/// URI of the LDAP server.
|
||||||
|
///
|
||||||
|
/// example: "ldap://ldap.example.com:389"
|
||||||
|
#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
|
||||||
|
pub uri: Url,
|
||||||
|
|
||||||
|
/// Whether to use StartTLS to bind to the LDAP server.
|
||||||
|
///
|
||||||
|
/// example: true
|
||||||
|
#[serde(default)]
|
||||||
|
pub start_tls: bool,
|
||||||
|
|
||||||
|
/// Root of the searches.
|
||||||
|
///
|
||||||
|
/// example: "ou=users,dc=example,dc=org"
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_dn: String,
|
||||||
|
|
||||||
|
/// Bind DN if anonymous search is not enabled.
|
||||||
|
///
|
||||||
|
/// example: "cn=ldap-reader,dc=example,dc=org"
|
||||||
|
#[serde(default)]
|
||||||
|
pub bind_dn: Option<String>,
|
||||||
|
|
||||||
|
/// Path to a file on the system that contains the password for the
|
||||||
|
/// `bind_dn`.
|
||||||
|
///
|
||||||
|
/// The server must be able to access the file, and it must not be empty.
|
||||||
|
#[serde(default)]
|
||||||
|
pub bind_password_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Search filter to limit user searches.
|
||||||
|
///
|
||||||
|
/// example: "(&(objectClass=person)(memberOf=matrix))"
|
||||||
|
///
|
||||||
|
/// default: "(objectClass=*)"
|
||||||
|
#[serde(default = "default_ldap_search_filter")]
|
||||||
|
pub filter: String,
|
||||||
|
|
||||||
|
/// Attribute to use to uniquely identify the user.
|
||||||
|
///
|
||||||
|
/// example: "uid" or "cn"
|
||||||
|
///
|
||||||
|
/// default: "uid"
|
||||||
|
#[serde(default = "default_ldap_uid_attribute")]
|
||||||
|
pub uid_attribute: String,
|
||||||
|
|
||||||
|
/// Attribute containing the mail of the user.
|
||||||
|
///
|
||||||
|
/// example: "mail"
|
||||||
|
#[serde(default = "default_ldap_mail_attribute")]
|
||||||
|
pub mail_attribute: String,
|
||||||
|
|
||||||
|
/// Attribute containing the distinguished name of the user.
|
||||||
|
///
|
||||||
|
/// example: "givenName" or "sn"
|
||||||
|
#[serde(default = "default_ldap_name_attribute")]
|
||||||
|
pub name_attribute: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
struct ListeningPort {
|
struct ListeningPort {
|
||||||
@@ -2267,10 +2347,16 @@ fn default_sender_shutdown_timeout() -> u64 { 5 }
|
|||||||
|
|
||||||
// blurhashing defaults recommended by https://blurha.sh/
|
// blurhashing defaults recommended by https://blurha.sh/
|
||||||
// 2^25
|
// 2^25
|
||||||
pub(super) fn default_blurhash_max_raw_size() -> u64 { 33_554_432 }
|
fn default_blurhash_max_raw_size() -> u64 { 33_554_432 }
|
||||||
|
|
||||||
pub(super) fn default_blurhash_x_component() -> u32 { 4 }
|
fn default_blurhash_x_component() -> u32 { 4 }
|
||||||
|
|
||||||
pub(super) fn default_blurhash_y_component() -> u32 { 3 }
|
fn default_blurhash_y_component() -> u32 { 3 }
|
||||||
|
|
||||||
// end recommended & blurhashing defaults
|
fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() }
|
||||||
|
|
||||||
|
fn default_ldap_uid_attribute() -> String { String::from("uid") }
|
||||||
|
|
||||||
|
fn default_ldap_mail_attribute() -> String { String::from("mail") }
|
||||||
|
|
||||||
|
fn default_ldap_name_attribute() -> String { String::from("name") }
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ pub enum Error {
|
|||||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IntoHttp(#[from] ruma::api::error::IntoHttpError),
|
IntoHttp(#[from] ruma::api::error::IntoHttpError),
|
||||||
|
#[error("{0}")]
|
||||||
|
Ldap(Cow<'static, str>),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Mxc(#[from] ruma::MxcUriError),
|
Mxc(#[from] ruma::MxcUriError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ image.workspace = true
|
|||||||
image.optional = true
|
image.optional = true
|
||||||
ipaddress.workspace = true
|
ipaddress.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
ldap3.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
loole.workspace = true
|
loole.workspace = true
|
||||||
lru-cache.workspace = true
|
lru-cache.workspace = true
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ pub async fn create_admin_room(services: &Services) -> Result {
|
|||||||
|
|
||||||
// Create a user for the server
|
// Create a user for the server
|
||||||
let server_user = services.globals.server_user.as_ref();
|
let server_user = services.globals.server_user.as_ref();
|
||||||
services.users.create(server_user, None, None).await?;
|
services
|
||||||
|
.users
|
||||||
|
.create(server_user, None, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let create_content = {
|
let create_content = {
|
||||||
use RoomVersionId::*;
|
use RoomVersionId::*;
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ impl Service {
|
|||||||
#[allow(clippy::collapsible_if)]
|
#[allow(clippy::collapsible_if)]
|
||||||
if !self.services.globals.user_is_local(user_id) {
|
if !self.services.globals.user_is_local(user_id) {
|
||||||
if !self.services.users.exists(user_id).await {
|
if !self.services.users.exists(user_id).await {
|
||||||
self.services.users.create(user_id, None, None).await?;
|
self.services
|
||||||
|
.users
|
||||||
|
.create(user_id, None, None)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::{collections::BTreeMap, mem, sync::Arc};
|
use std::{collections::BTreeMap, mem, sync::Arc};
|
||||||
|
|
||||||
use futures::{Stream, StreamExt, TryFutureExt};
|
use futures::{Stream, StreamExt, TryFutureExt};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
||||||
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
||||||
@@ -13,8 +15,8 @@ use ruma::{
|
|||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, Server, at, debug_warn, err, trace,
|
Err, Error, Result, Server, at, debug, debug_warn, err, error, trace,
|
||||||
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
utils::{self, ReadyExt, result::LogErr, stream::TryIgnore, string::Unquoted},
|
||||||
};
|
};
|
||||||
use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map};
|
use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||||
|
|
||||||
@@ -123,6 +125,10 @@ impl Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new user account on this homeserver.
|
/// 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]
|
#[inline]
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
&self,
|
&self,
|
||||||
@@ -222,7 +228,11 @@ impl Service {
|
|||||||
|
|
||||||
/// Returns the origin of the user (password/LDAP/...).
|
/// Returns the origin of the user (password/LDAP/...).
|
||||||
pub async fn origin(&self, user_id: &UserId) -> Result<String> {
|
pub async fn origin(&self, user_id: &UserId) -> Result<String> {
|
||||||
self.db.userid_origin.get(user_id).await.deserialized()
|
self.db
|
||||||
|
.userid_origin
|
||||||
|
.get(user_id)
|
||||||
|
.await
|
||||||
|
.deserialized()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the password hash for the given user.
|
/// Returns the password hash for the given user.
|
||||||
@@ -236,13 +246,17 @@ impl Service {
|
|||||||
|
|
||||||
/// Hash and set the user's password to the Argon2 hash
|
/// Hash and set the user's password to the Argon2 hash
|
||||||
pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
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 self
|
if self
|
||||||
.db
|
.db
|
||||||
.userid_origin
|
.userid_origin
|
||||||
.get(user_id)
|
.get(user_id)
|
||||||
.await
|
.await
|
||||||
.deserialized::<String>()?
|
.deserialized::<String>()?
|
||||||
== "ldap"
|
== "ldap" && password.is_some()
|
||||||
|
&& password != Some("*")
|
||||||
{
|
{
|
||||||
Err!(Request(InvalidParam("Cannot change password of a LDAP user")))
|
Err!(Request(InvalidParam("Cannot change password of a LDAP user")))
|
||||||
} else {
|
} else {
|
||||||
@@ -1163,6 +1177,96 @@ impl Service {
|
|||||||
self.db.useridprofilekey_value.del(key);
|
self.db.useridprofilekey_value.del(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<String>> {
|
||||||
|
let config = &self.services.server.config.ldap;
|
||||||
|
let (conn, mut ldap) = LdapConnAsync::new(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 filter = &config.filter;
|
||||||
|
|
||||||
|
let (entries, _result) = ldap
|
||||||
|
.search(&config.base_dn, Scope::Subtree, filter, &attr)
|
||||||
|
.await
|
||||||
|
.and_then(ldap3::SearchResult::success)
|
||||||
|
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search"))
|
||||||
|
.map_err(|e| err!(Ldap(error!(?attr, ?filter, "LDAP search error: {e}"))))?;
|
||||||
|
|
||||||
|
let localpart = user_id.localpart().to_owned();
|
||||||
|
let lowercased_localpart = localpart.to_lowercase();
|
||||||
|
let dns = 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)
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
ldap.unbind()
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?;
|
||||||
|
|
||||||
|
driver.await.log_err().ok();
|
||||||
|
|
||||||
|
Ok(dns)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result {
|
||||||
|
let config = &self.services.server.config.ldap;
|
||||||
|
let (conn, mut ldap) = LdapConnAsync::new(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(
|
pub fn parse_master_key(
|
||||||
|
|||||||
@@ -1626,3 +1626,66 @@
|
|||||||
# is 33.55MB. Setting it to 0 disables blurhashing.
|
# is 33.55MB. Setting it to 0 disables blurhashing.
|
||||||
#
|
#
|
||||||
#blurhash_max_raw_size = 33554432
|
#blurhash_max_raw_size = 33554432
|
||||||
|
|
||||||
|
[global.ldap]
|
||||||
|
|
||||||
|
# Whether to enable LDAP login.
|
||||||
|
#
|
||||||
|
# example: "true"
|
||||||
|
#
|
||||||
|
#enable = false
|
||||||
|
|
||||||
|
# URI of the LDAP server.
|
||||||
|
#
|
||||||
|
# example: "ldap://ldap.example.com:389"
|
||||||
|
#
|
||||||
|
#uri =
|
||||||
|
|
||||||
|
# Whether to use StartTLS to bind to the LDAP server.
|
||||||
|
#
|
||||||
|
# example: true
|
||||||
|
#
|
||||||
|
#start_tls = false
|
||||||
|
|
||||||
|
# Root of the searches.
|
||||||
|
#
|
||||||
|
# example: "ou=users,dc=example,dc=org"
|
||||||
|
#
|
||||||
|
#base_dn = false
|
||||||
|
|
||||||
|
# Bind DN if anonymous search is not enabled.
|
||||||
|
#
|
||||||
|
# example: "cn=ldap-reader,dc=example,dc=org"
|
||||||
|
#
|
||||||
|
#bind_dn = false
|
||||||
|
|
||||||
|
# Path to a file on the system that contains the password for the
|
||||||
|
# `bind_dn`.
|
||||||
|
#
|
||||||
|
# The server must be able to access the file, and it must not be empty.
|
||||||
|
#
|
||||||
|
#bind_password_file = false
|
||||||
|
|
||||||
|
# Search filter to limit user searches.
|
||||||
|
#
|
||||||
|
# example: "(&(objectClass=person)(memberOf=matrix))"
|
||||||
|
#
|
||||||
|
#filter = "(objectClass=*)"
|
||||||
|
|
||||||
|
# Attribute to use to uniquely identify the user.
|
||||||
|
#
|
||||||
|
# example: "uid" or "cn"
|
||||||
|
#
|
||||||
|
#uid_attribute = "uid"
|
||||||
|
|
||||||
|
# Attribute containing the mail of the user.
|
||||||
|
#
|
||||||
|
# example: "mail"
|
||||||
|
#
|
||||||
|
#mail_attribute =
|
||||||
|
|
||||||
|
# Attribute containing the distinguished name of the user.
|
||||||
|
#
|
||||||
|
# example: "givenName" or "sn"
|
||||||
|
#
|
||||||
|
#name_attribute =
|
||||||
|
|||||||
Reference in New Issue
Block a user