Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot 91e8c0ea71 chore(deps): update https://github.com/softprops/action-gh-release action to v3 2026-05-01 17:50:20 +00:00
41 changed files with 999 additions and 432 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ runs:
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
uses: https://github.com/taiki-e/install-action@787505cde8a44ea468a00478fe52baf23b15bccd # v2
with:
tool: git-warp-time,timelord-cli@3.0.1
+2
View File
@@ -45,6 +45,7 @@
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
myself, if applicable. This includes ensuring code compiles.
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
<!--
Notes on these requirements:
@@ -78,3 +79,4 @@
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
+1 -1
View File
@@ -216,7 +216,7 @@ jobs:
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@v2
uses: https://github.com/softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
draft: true
files: binaries/*
Generated
+331 -197
View File
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -39,10 +39,7 @@ features = ["ffi", "std", "union"]
version = "1.1.0"
[workspace.dependencies.ctor]
version = "0.13.0"
[workspace.dependencies.dtor]
version = "0.13.0"
version = "0.10.0"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -433,7 +430,7 @@ features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.48.0"
version = "0.47.0"
default-features = false
features = [
"backtrace",
@@ -448,9 +445,9 @@ features = [
]
[workspace.dependencies.sentry-tracing]
version = "0.48.0"
version = "0.47.0"
[workspace.dependencies.sentry-tower]
version = "0.48.0"
version = "0.47.0"
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]
@@ -549,11 +546,16 @@ features = ["std"]
[workspace.dependencies.maplit]
version = "1.0.2"
[workspace.dependencies.ldap3]
version = "0.12.0"
default-features = false
features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.16.0"
version = "0.15.0"
[workspace.dependencies.lettre]
version = "0.11.19"
-1
View File
@@ -1 +0,0 @@
Removed support for LDAP.
-1
View File
@@ -1 +0,0 @@
Clarified in the config that `max_request_size` affects federated media as well.
-1
View File
@@ -1 +0,0 @@
Added support for fallback encryption keys.
+96 -1
View File
@@ -291,7 +291,6 @@
#ip_lookup_strategy = 5
# Max request size for file uploads in bytes. Defaults to 20MB.
# Also limits incoming federated media.
#
#max_request_size = 20971520
@@ -1934,6 +1933,102 @@
#
#foci = []
[global.ldap]
# Whether to enable LDAP login.
#
# example: "true"
#
#enable = false
# Whether to force LDAP authentication or authorize classical password
# login.
#
# example: "true"
#
#ldap_only = false
# URI of the LDAP server.
#
# example: "ldap://ldap.example.com:389"
#
#uri = ""
# StartTLS for LDAP connections.
#
#use_starttls = false
# Skip TLS certificate verification, possibly dangerous.
#
#disable_tls_verification = false
# Root of the searches.
#
# example: "ou=users,dc=example,dc=org"
#
#base_dn = ""
# Bind DN if anonymous search is not enabled.
#
# You can use the variable `{username}` that will be replaced by the
# entered username. In such case, the password used to bind will be the
# one provided for the login and not the one given by
# `bind_password_file`. Beware: automatically granting admin rights will
# not work if you use this direct bind instead of a LDAP search.
#
# example: "cn=ldap-reader,dc=example,dc=org" or
# "cn={username},ou=users,dc=example,dc=org"
#
#bind_dn = ""
# 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 = ""
# Search filter to limit user searches.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# 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 display name of the user.
#
# example: "givenName" or "sn"
#
#name_attribute = "givenName"
# Root of the searches for admin users.
#
# Defaults to `base_dn` if empty.
#
# example: "ou=admins,dc=example,dc=org"
#
#admin_base_dn = ""
# The LDAP search filter to find administrative users for continuwuity.
#
# If left blank, administrative state must be configured manually for each
# user.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
#
#admin_filter = ""
#[global.antispam]
#[global.antispam.meowlnir]
+1 -1
View File
@@ -50,7 +50,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.19.0
ENV BINSTALL_VERSION=1.18.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.19.0
ENV BINSTALL_VERSION=1.18.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
Generated
+15 -15
View File
@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1777645914,
"narHash": "sha256-P1T7QVQS13OvkXEuEhI91CLaQfyv6iqV9vW8IBLLDYg=",
"lastModified": 1775907537,
"narHash": "sha256-vbeLNgmsx1Z6TwnlDV0dKyeBCcon3UpkV9yLr/yc6HM=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "d6ba1f7070ba91f45efe372d68eb648be67d0417",
"rev": "d99f7b9eb81731bddebf80a355f8be7b2f8b1b28",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1777335812,
"narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=",
"lastModified": 1775839657,
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "5e0fb2f64edff2822249f21293b8304dedaaf676",
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1777624102,
"narHash": "sha256-thSyElkje577x/kAbP72nHlfiFc1a+tCudskLPHXe9s=",
"lastModified": 1775891769,
"narHash": "sha256-EOfVlTKw2n8w1uhfh46GS4hEGnQ7oWrIWQfIY6utIkI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4d81601e0b73f20d81d066754ad0e7d1e7f75a06",
"rev": "6fbc54dde15aee725bdc7aae5e478849685d5f56",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1777583169,
"narHash": "sha256-dVJ4+wrRKc8oIgp3rLOFSq1obt/sCKlXy3h47qof/w0=",
"lastModified": 1775843361,
"narHash": "sha256-j53ZgyDvmYf3Sjh1IPvvTjqa614qUfVQSzj59+MpzkY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "aa64e4828a2bbba44463c1229a81c748d3cce583",
"rev": "9eb97ea96d8400e8957ddd56702e962614296583",
"type": "github"
},
"original": {
+17 -17
View File
@@ -1399,9 +1399,9 @@
}
},
"node_modules/hookable": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz",
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
"integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
"dev": true,
"license": "MIT"
},
@@ -2683,20 +2683,20 @@
"license": "MIT"
},
"node_modules/oniguruma-parser": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"dev": true,
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
"integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.2",
"oniguruma-parser": "^0.12.1",
"regex": "^6.1.0",
"regex-recursion": "^6.0.2"
}
@@ -2822,9 +2822,9 @@
}
},
"node_modules/react-router": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
"integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2845,13 +2845,13 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz",
"integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-router": "7.14.2"
"react-router": "7.14.0"
},
"engines": {
"node": ">=20.0.0"
-1
View File
@@ -81,7 +81,6 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
lettre.workspace = true
log.workspace = true
+1 -1
View File
@@ -70,7 +70,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// Create user
self.services
.users
.create(&user_id, Some(password.as_str()))
.create(&user_id, Some(password.as_str()), None)
.await?;
// Default to pretty displayname
+3 -1
View File
@@ -48,6 +48,9 @@ jemalloc_stats = [
"conduwuit-core/jemalloc_stats",
"conduwuit-service/jemalloc_stats",
]
ldap = [
"conduwuit-service/ldap"
]
release_max_log_level = [
"conduwuit-core/release_max_log_level",
"conduwuit-service/release_max_log_level",
@@ -74,7 +77,6 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
hmac.workspace = true
http.workspace = true
+1 -1
View File
@@ -190,7 +190,7 @@ pub(crate) async fn register_route(
let password = if is_guest { None } else { body.password.as_deref() };
// Create user
services.users.create(&user_id, password).await?;
services.users.create(&user_id, password, None).await?;
// Set an initial display name
let mut displayname = user_id.localpart().to_owned();
-21
View File
@@ -64,27 +64,6 @@ pub(crate) async fn upload_keys_route(
.await?;
}
for (key_id, fallback_key) in &body.fallback_keys {
if fallback_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
%key_id,
?fallback_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
services
.users
.add_fallback_key(sender_user, sender_device, key_id, fallback_key, false)
.await?;
}
if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize().map_err(|e| {
err!(Request(BadJson(debug_warn!(
+89 -2
View File
@@ -7,7 +7,7 @@
utils::{self, ReadyExt, hash, stream::BroadbandExt},
warn,
};
use conduwuit_core::debug_error;
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::Services;
use futures::StreamExt;
use lettre::Address;
@@ -64,6 +64,17 @@ pub(crate) async fn password_login(
lowercased_user_id: &UserId,
password: &str,
) -> Result<OwnedUserId> {
// Restrict login to accounts only of type 'password', including untyped
// legacy accounts which are equivalent to 'password'.
if services
.users
.origin(user_id)
.await
.is_ok_and(|origin| origin != "password")
{
return Err!(Request(Forbidden("Account does not permit password login.")));
}
let (hash, user_id) = match services.users.password_hash(user_id).await {
| Ok(hash) => (hash, user_id),
| Err(_) => services
@@ -85,6 +96,71 @@ pub(crate) async fn password_login(
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.
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap", level = "debug")]
pub(super) async fn ldap_login(
services: &Services,
user_id: &UserId,
lowercased_user_id: &UserId,
password: &str,
) -> Result<OwnedUserId> {
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
| Some(bind_dn) if bind_dn.contains("{username}") =>
(bind_dn.replace("{username}", lowercased_user_id.localpart()), None),
| _ => {
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, is_admin)) = dns.first() else {
return password_login(services, user_id, lowercased_user_id, password).await;
};
(user_dn.clone(), *is_admin)
},
};
let user_id = services
.users
.auth_ldap(&user_dn, password)
.await
.map(|()| lowercased_user_id.to_owned())?;
// 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 conduwuit 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 {
services
.users
.create(lowercased_user_id, Some("*"), Some("ldap"))
.await?;
}
// Only sync admin status if LDAP can actually determine it.
// None means LDAP cannot determine admin status (manual config required).
if let Some(is_ldap_admin) = is_ldap_admin {
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
if is_ldap_admin && !is_conduwuit_admin {
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
} else if !is_ldap_admin && is_conduwuit_admin {
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
}
}
Ok(user_id)
}
pub(crate) async fn handle_login(
services: &Services,
identifier: Option<&UserIdentifier>,
@@ -136,7 +212,18 @@ pub(crate) async fn handle_login(
return Err!(Request(Forbidden("This account is not permitted to log in.")));
}
password_login(services, &user_id, &lowercased_user_id, password).await
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
| Err(err) if services.config.ldap.ldap_only => Err(err),
| Err(err) => {
debug_warn!("{err}");
password_login(services, &user_id, &lowercased_user_id, password).await
},
}
} else {
password_login(services, &user_id, &lowercased_user_id, password).await
}
}
/// # `POST /_matrix/client/v3/login`
+3 -8
View File
@@ -395,10 +395,6 @@ pub(crate) async fn build_sync_events(
.users
.count_one_time_keys(syncing_user, syncing_device);
let unused_fallback_key_types = services
.users
.list_unused_fallback_key_types(syncing_user, syncing_device);
let (
(joined_rooms, mut device_list_updates),
left_rooms,
@@ -409,7 +405,6 @@ pub(crate) async fn build_sync_events(
to_device_events,
keys_changed,
device_one_time_keys_count,
unused_fallback_key_types,
) = async {
futures::join!(
joined_rooms,
@@ -420,8 +415,7 @@ pub(crate) async fn build_sync_events(
account_data,
to_device_events,
keys_changed,
device_one_time_keys_count,
unused_fallback_key_types,
device_one_time_keys_count
)
}
.boxed()
@@ -439,7 +433,8 @@ pub(crate) async fn build_sync_events(
account_data: assign!(GlobalAccountData::new(), { events: account_data }),
device_lists: device_list_updates.into(),
device_one_time_keys_count,
device_unused_fallback_key_types: Some(unused_fallback_key_types),
// Fallback keys are not yet supported
device_unused_fallback_key_types: None,
presence: assign!(Presence::new(), {
events: presence_updates
.into_iter()
-1
View File
@@ -70,7 +70,6 @@ conduwuit-build-metadata.workspace = true
const-str.workspace = true
core_affinity.workspace = true
ctor.workspace = true
dtor.workspace = true
cyborgtime.workspace = true
either.workspace = true
figment.workspace = true
+1 -1
View File
@@ -47,7 +47,7 @@
const NAME_MAX: usize = 128;
const KEY_SEGS: usize = 8;
#[ctor::ctor(unsafe)]
#[ctor::ctor]
fn _static_initialization() {
acq_epoch().expect("pre-initialization of jemalloc failed");
acq_epoch().expect("pre-initialization of jemalloc failed");
+130 -1
View File
@@ -371,7 +371,6 @@ pub struct Config {
pub ip_lookup_strategy: u8,
/// Max request size for file uploads in bytes. Defaults to 20MB.
/// Also limits incoming federated media.
///
/// default: 20971520
#[serde(default = "default_max_request_size")]
@@ -2130,6 +2129,10 @@ pub struct Config {
#[serde(default)]
pub allow_web_indexing: bool,
/// display: nested
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
/// display: nested
#[serde(default)]
@@ -2291,6 +2294,126 @@ pub fn effective_foci(&self, deprecated_foci: &[RtcFocusInfo]) -> Vec<RtcTranspo
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
pub struct LdapConfig {
/// Whether to enable LDAP login.
///
/// example: "true"
#[serde(default)]
pub enable: bool,
/// Whether to force LDAP authentication or authorize classical password
/// login.
///
/// example: "true"
#[serde(default)]
pub ldap_only: bool,
/// URI of the LDAP server.
///
/// example: "ldap://ldap.example.com:389"
///
/// default: ""
#[serde(default)]
pub uri: Option<Url>,
/// StartTLS for LDAP connections.
///
/// default: false
#[serde(default)]
pub use_starttls: bool,
/// Skip TLS certificate verification, possibly dangerous.
///
/// default: false
#[serde(default)]
pub disable_tls_verification: bool,
/// Root of the searches.
///
/// example: "ou=users,dc=example,dc=org"
///
/// default: ""
#[serde(default)]
pub base_dn: String,
/// Bind DN if anonymous search is not enabled.
///
/// You can use the variable `{username}` that will be replaced by the
/// entered username. In such case, the password used to bind will be the
/// one provided for the login and not the one given by
/// `bind_password_file`. Beware: automatically granting admin rights will
/// not work if you use this direct bind instead of a LDAP search.
///
/// example: "cn=ldap-reader,dc=example,dc=org" or
/// "cn={username},ou=users,dc=example,dc=org"
///
/// default: ""
#[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.
///
/// default: ""
#[serde(default)]
pub bind_password_file: Option<PathBuf>,
/// Search filter to limit user searches.
///
/// You can use the variable `{username}` that will be replaced by the
/// entered username for more complex filters.
///
/// 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 display name of the user.
///
/// example: "givenName" or "sn"
///
/// default: "givenName"
#[serde(default = "default_ldap_name_attribute")]
pub name_attribute: String,
/// Root of the searches for admin users.
///
/// Defaults to `base_dn` if empty.
///
/// example: "ou=admins,dc=example,dc=org"
///
/// default: ""
#[serde(default)]
pub admin_base_dn: String,
/// The LDAP search filter to find administrative users for continuwuity.
///
/// If left blank, administrative state must be configured manually for each
/// user.
///
/// You can use the variable `{username}` that will be replaced by the
/// entered username for more complex filters.
///
/// example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
///
/// default: ""
#[serde(default)]
pub admin_filter: String,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(transparent)]
struct ListeningPort {
@@ -2811,3 +2934,9 @@ pub(super) fn default_blurhash_x_component() -> u32 { 4 }
pub(super) 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_name_attribute() -> String { String::from("givenName") }
+1 -1
View File
@@ -62,7 +62,7 @@ macro_rules! debug_info {
pub static DEBUGGER: LazyLock<bool> =
LazyLock::new(|| env::var("_").unwrap_or_default().ends_with("gdb"));
#[cfg_attr(debug_assertions, ctor::ctor(unsafe))]
#[cfg_attr(debug_assertions, ctor::ctor)]
#[cfg_attr(not(debug_assertions), allow(dead_code))]
fn set_panic_trap() {
if !*DEBUGGER {
+2
View File
@@ -110,6 +110,8 @@ pub enum Error {
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
#[error(transparent)]
IntoHttp(#[from] ruma::api::error::IntoHttpError),
#[error("{0}")]
Ldap(Cow<'static, str>),
#[error(transparent)]
Mxc(#[from] ruma::MxcUriError),
#[error(transparent)]
-1
View File
@@ -58,7 +58,6 @@ conduwuit-core.workspace = true
conduwuit-macros.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
log.workspace = true
minicbor.workspace = true
+2 -8
View File
@@ -288,14 +288,8 @@ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
}
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let byte = self
.buf
.get(self.pos)
.ok_or(Self::Error::SerdeDe("bool buffer underflow".into()))?;
self.inc_pos(1);
visitor.visit_bool(*byte != 0x00)
fn deserialize_bool<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
unhandled!("deserialize bool not implemented")
}
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
-4
View File
@@ -120,10 +120,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "onetimekeyid_onetimekeys",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "fallbackkeyid_fallbackkey",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "passwordresettoken_info",
..descriptor::RANDOM_SMALL
+2 -2
View File
@@ -297,8 +297,8 @@ fn serialize_u16(self, _v: u16) -> Result<Self::Ok> {
fn serialize_u8(self, v: u8) -> Result<Self::Ok> { self.write(&[v]) }
fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
if v { self.write(&[0x01]) } else { self.write(&[0x00]) }
fn serialize_bool(self, _v: bool) -> Result<Self::Ok> {
unhandled!("serialize bool not implemented")
}
fn serialize_unit(self) -> Result<Self::Ok> { unhandled!("serialize unit not implemented") }
+2 -2
View File
@@ -32,11 +32,11 @@ mod __compile_introspection {
const CRATE_NAME: &str = #crate_name;
/// Register this crate's features with the global registry during static initialization
#[::ctor::ctor(unsafe)]
#[::ctor::ctor]
fn register() {
conduwuit_core::info::introspection::ENABLED_FEATURES.lock().unwrap().insert(#crate_name, &ENABLED);
}
#[::dtor::dtor(unsafe)]
#[::ctor::dtor]
fn unregister() {
conduwuit_core::info::introspection::ENABLED_FEATURES.lock().unwrap().remove(#crate_name);
}
+18 -16
View File
@@ -42,27 +42,27 @@ assets = [
[features]
default = [
"standard",
#"release_max_log_level",
"release_max_log_level",
"ring",
"bindgen-runtime", # replace with bindgen-static on alpine
"bindgen-runtime", # replace with bindgen-static on alpine
]
standard = [
"blurhashing",
"brotli_compression",
"element_hacks",
"gzip_compression",
"io_uring",
"jemalloc",
"jemalloc_conf",
"journald",
"media_thumbnail",
"systemd",
"url_preview",
"zstd_compression",
"brotli_compression",
"element_hacks",
"gzip_compression",
"io_uring",
"jemalloc",
"jemalloc_conf",
"journald",
"ldap",
"media_thumbnail",
"systemd",
"url_preview",
"zstd_compression",
"sentry_telemetry",
"otlp_telemetry",
"console",
"direct_tls",
"console",
]
full = [
"standard",
@@ -126,6 +126,9 @@ jemalloc_stats = [
jemalloc_conf = [
"conduwuit-core/jemalloc_conf",
]
ldap = [
"conduwuit-api/ldap",
]
media_thumbnail = [
"conduwuit-service/media_thumbnail",
]
@@ -214,7 +217,6 @@ conduwuit-macros.workspace = true
clap.workspace = true
ctor.workspace = true
dtor.workspace = true
console-subscriber.optional = true
console-subscriber.workspace = true
const-str.workspace = true
-1
View File
@@ -105,7 +105,6 @@ conduwuit-service.workspace = true
conduwuit-web.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
http.workspace = true
http-body-util.workspace = true
+5 -1
View File
@@ -52,6 +52,9 @@ jemalloc_stats = [
"conduwuit-core/jemalloc_stats",
"conduwuit-database/jemalloc_stats",
]
ldap = [
"dep:ldap3"
]
media_thumbnail = [
"dep:image",
]
@@ -86,7 +89,6 @@ conduwuit-database.workspace = true
conduwuit-macros.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
either.workspace = true
futures.workspace = true
governor.workspace = true
@@ -96,6 +98,8 @@ image.workspace = true
image.optional = true
ipaddress.workspace = true
itertools.workspace = true
ldap3.workspace = true
ldap3.optional = true
log.workspace = true
loole.workspace = true
lru-cache.workspace = true
+1 -1
View File
@@ -37,7 +37,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
// Create a user for the server
let server_user = services.globals.server_user.as_ref();
services.users.create(server_user, None).await?;
services.users.create(server_user, None, None).await?;
let mut create_content = {
use RoomVersionId::*;
+1 -1
View File
@@ -111,7 +111,7 @@ async fn start_appservice(&self, id: String, registration: Registration) -> Resu
if !self.services.users.exists(&appservice_user_id).await {
self.services
.users
.create(&appservice_user_id, None)
.create(&appservice_user_id, None, None)
.await?;
} else if self
.services
+5
View File
@@ -37,6 +37,11 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
}
async fn worker(self: Arc<Self>) -> Result {
if self.services.config.ldap.enable {
warn!("emergency password feature not available with LDAP enabled.");
return Ok(());
}
self.set_emergency_access().await.inspect_err(|e| {
error!("Could not set the configured emergency password for the server user: {e}");
})
+11
View File
@@ -58,6 +58,17 @@ pub async fn issue_token(&self, user_id: OwnedUserId) -> Result<ValidResetToken>
return Err!("Cannot issue a password reset token for the server user");
}
if self
.services
.users
.origin(&user_id)
.await
.unwrap_or_else(|_| "password".to_owned())
!= "password"
{
return Err!("Cannot issue a password reset token for non-internal user {user_id}");
}
if self.services.users.is_deactivated(&user_id).await? {
return Err!("Cannot issue a password reset token for deactivated user {user_id}");
}
+1 -1
View File
@@ -47,7 +47,7 @@ pub async fn update_membership(
#[allow(clippy::collapsible_if)]
if !self.services.globals.user_is_local(user_id) {
if !self.services.users.exists(user_id).await {
self.services.users.create(user_id, None).await?;
self.services.users.create(user_id, None, None).await?;
}
}
+17
View File
@@ -385,6 +385,23 @@ async fn check_stage(
password_verified = hash::verify_password(password, &hash).is_ok();
}
// If local password verification failed, try LDAP authentication
#[cfg(feature = "ldap")]
if !password_verified && self.services.config.ldap.enable {
// Search for user in LDAP to get their DN
if let Ok(dns) = self.services.users.search_ldap(&user_id).await {
if let Some((user_dn, _is_admin)) = dns.first() {
// Try to authenticate with LDAP
password_verified = self
.services
.users
.auth_ldap(user_dn, password)
.await
.is_ok();
}
}
}
if password_verified {
identity.try_set_localpart(user_id.localpart().to_owned())?;
+227 -109
View File
@@ -1,16 +1,24 @@
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, trace,
Err, Error, Result, Server, debug_warn, err, is_equal_to, 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, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName,
OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedOneTimeKeyId, OwnedUserId, RoomId, UInt, UserId,
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
api::{
client::{device::Device, filter::FilterDefinition},
error::ErrorKind,
@@ -57,7 +65,6 @@ struct Data {
keychangeid_userid: Arc<Map>,
keyid_key: Arc<Map>,
onetimekeyid_onetimekeys: Arc<Map>,
fallbackkeyid_fallbackkey: Arc<Map>,
openidtoken_expiresatuserid: Arc<Map>,
logintoken_expiresatuserid: Arc<Map>,
todeviceid_events: Arc<Map>,
@@ -72,6 +79,7 @@ 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>,
@@ -98,7 +106,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
keychangeid_userid: args.db["keychangeid_userid"].clone(),
keyid_key: args.db["keyid_key"].clone(),
onetimekeyid_onetimekeys: args.db["onetimekeyid_onetimekeys"].clone(),
fallbackkeyid_fallbackkey: args.db["fallbackkeyid_fallbackkey"].clone(),
openidtoken_expiresatuserid: args.db["openidtoken_expiresatuserid"].clone(),
logintoken_expiresatuserid: args.db["logintoken_expiresatuserid"].clone(),
todeviceid_events: args.db["todeviceid_events"].clone(),
@@ -113,6 +120,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
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(),
@@ -170,12 +178,26 @@ pub async fn is_admin(&self, user_id: &UserId) -> bool {
}
/// 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>) -> Result<()> {
if !self.services.globals.user_is_local(user_id) && password.is_some() {
return Err!("Cannot create a nonlocal user with a set password");
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");
}
self.db
.userid_origin
.insert(user_id, origin.unwrap_or("password"));
self.set_password(user_id, password).await?;
Ok(())
@@ -338,6 +360,11 @@ pub fn list_local_users(&self) -> impl Stream<Item = OwnedUserId> + Send + '_ {
.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()
@@ -345,6 +372,22 @@ pub async fn password_hash(&self, user_id: &UserId) -> Result<String> {
/// 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()
@@ -552,7 +595,7 @@ pub async fn add_one_time_key(
&self,
user_id: &UserId,
device_id: &DeviceId,
one_time_key_key: &OneTimeKeyId,
one_time_key_key: &KeyId<OneTimeKeyAlgorithm, OneTimeKeyName>,
one_time_key_value: &Raw<OneTimeKey>,
) -> Result {
// All devices have metadata
@@ -589,39 +632,6 @@ pub async fn add_one_time_key(
Ok(())
}
/// Save a fallback key for the given user, device, and algorithm
/// This key will replace an existing fallback key
pub async fn add_fallback_key(
&self,
user_id: &UserId,
device_id: &DeviceId,
fallback_key_id: &OneTimeKeyId,
fallback_key: &Raw<OneTimeKey>,
used: bool,
) -> Result {
// All devices have metadata
// Only existing devices should be able to call this, but we shouldn't assert
// either...
let key = (user_id, device_id);
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
return Err!(Database(error!(
%user_id,
%device_id,
"User does not exist or device has no metadata."
)));
}
// There is one fallback key slot per user, per device, per algorithm
// Therefore we use this as the DB key for this column
let db_key = (user_id, device_id, fallback_key_id.algorithm());
self.db
.fallbackkeyid_fallbackkey
.put(db_key, (used, fallback_key_id.as_str(), Json(fallback_key)));
Ok(())
}
pub async fn last_one_time_keys_update(&self, user_id: &UserId) -> u64 {
self.db
.userid_lastonetimekeyupdate
@@ -653,8 +663,6 @@ pub async fn take_one_time_key(
.onetimekeyid_onetimekeys
.raw_stream_prefix(&prefix)
.ignore_err()
.next()
.await
.map(|(key, val)| {
self.db.onetimekeyid_onetimekeys.remove(key);
@@ -673,44 +681,11 @@ pub async fn take_one_time_key(
.unwrap();
(key, val)
});
})
.next()
.await;
if let Some(result) = one_time_key {
return Ok(result);
}
// No one-time key has been found. Look for a fallback key.
let db_key = (user_id, device_id, key_algorithm);
let fallback_key = self
.db
.fallbackkeyid_fallbackkey
.qry(&db_key)
.await
.ok()
.and_then(|handle| {
handle
.deserialized::<(bool, OwnedOneTimeKeyId, Raw<OneTimeKey>)>()
.ok()
});
if let Some((used, fallback_key_id, fallback_key_value)) = fallback_key {
if !used {
// write the key to the database again to mark it as used
self.add_fallback_key(
user_id,
device_id,
&fallback_key_id,
&fallback_key_value,
true,
)
.await?;
}
return Ok((fallback_key_id, fallback_key_value));
}
Err(err!(Request(NotFound("No one-time key or fallback key found"))))
one_time_key.ok_or_else(|| err!(Request(NotFound("No one-time-key found"))))
}
pub async fn count_one_time_keys(
@@ -743,34 +718,6 @@ pub async fn count_one_time_keys(
algorithm_counts
}
pub async fn list_unused_fallback_key_types(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Vec<OneTimeKeyAlgorithm> {
type KeyVal = ((String, String, OneTimeKeyAlgorithm), (bool, String, Ignore));
let mut query = user_id.as_bytes().to_vec();
query.push(0xFF);
query.extend_from_slice(device_id.as_bytes());
query.push(0xFF);
let mut unused_algorithms = Vec::new();
self.db
.fallbackkeyid_fallbackkey
.stream_prefix(&query)
.ignore_err()
.ready_for_each(|((_, _, fallback_key_algorithm), (used, ..)): KeyVal| {
if !used {
unused_algorithms.push(fallback_key_algorithm);
}
})
.await;
unused_algorithms
}
pub async fn add_device_keys(
&self,
user_id: &UserId,
@@ -1345,6 +1292,177 @@ pub async fn clear_profile(&self, user_id: &UserId) {
.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(
+1 -1
View File
@@ -14,7 +14,7 @@ conduwuit-admin.workspace = true
conduwuit.workspace = true
clap.workspace = true
askama = "0.16.0"
askama = "0.15.1"
cargo_metadata = "0.23.1"
[lints]