From ecb87ccd1c18126093628b1f424c3ed7f89e8ff9 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:39:36 +0200 Subject: [PATCH 01/17] chore(nix): bump rocksdb version in flake.nix to 10.4.fb This works without any further changes. Multiple people in the matrix room (including myself) have reported that the built executable runs fine with this. Nevertheless, there might be room for improvements (in future commits) --- flake.lock | 12 ++++++------ flake.nix | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 51a04c6c..4af82c75 100644 --- a/flake.lock +++ b/flake.lock @@ -516,16 +516,16 @@ "rocksdb": { "flake": false, "locked": { - "lastModified": 1741308171, - "narHash": "sha256-YdBvdQ75UJg5ffwNjxizpviCVwVDJnBkM8ZtGIduMgY=", - "ref": "v9.11.1", - "rev": "3ce04794bcfbbb0d2e6f81ae35fc4acf688b6986", - "revCount": 13177, + "lastModified": 1753385396, + "narHash": "sha256-/Hvy1yTH/0D5aa7bc+/uqFugCQq4InTdwlRw88vA5IY=", + "ref": "10.4.fb", + "rev": "28d4b7276c16ed3e28af1bd96162d6442ce25923", + "revCount": 13318, "type": "git", "url": "https://forgejo.ellis.link/continuwuation/rocksdb" }, "original": { - "ref": "v9.11.1", + "ref": "10.4.fb", "type": "git", "url": "https://forgejo.ellis.link/continuwuation/rocksdb" } diff --git a/flake.nix b/flake.nix index 564cd479..f0dcb6fb 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ nix-filter.url = "github:numtide/nix-filter?ref=main"; nixpkgs.url = "github:NixOS/nixpkgs?ref=nixpkgs-unstable"; rocksdb = { - url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=v9.11.1"; + url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=10.4.fb"; flake = false; }; }; @@ -62,7 +62,7 @@ }).overrideAttrs (old: { src = inputs.rocksdb; - version = "v9.11.1"; + version = "v10.4.fb"; cmakeFlags = pkgs.lib.subtractLists [ # No real reason to have snappy or zlib, no one uses this From 256bed992e50a86b54dc3ff0d24b8d72ce69904d Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:40:11 +0200 Subject: [PATCH 02/17] chore(nix): exec 'use flake' with direnv on NixOS systems --- .envrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.envrc b/.envrc index bad73b75..172993c4 100644 --- a/.envrc +++ b/.envrc @@ -2,6 +2,8 @@ dotenv_if_exists -# use flake ".#${DIRENV_DEVSHELL:-default}" +if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then + use flake ".#${DIRENV_DEVSHELL:-default}" +fi PATH_add bin From aacaf5a2a031c9e44c2fb9a081a6109a39e5ddbb Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Thu, 21 Aug 2025 21:10:15 +0100 Subject: [PATCH 03/17] fix(ci): Downgrade setup-uv action from v6 to v5 The setup-uv@v6 action has deprecated Node 18 support mid-version by using the File API, causing workflow failures. Temporarily downgrading to v5 until we migrate to a better runner image with Node 20+ support. --- .forgejo/workflows/prek-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/prek-checks.yml b/.forgejo/workflows/prek-checks.yml index 18f573bb..ac330ca2 100644 --- a/.forgejo/workflows/prek-checks.yml +++ b/.forgejo/workflows/prek-checks.yml @@ -18,7 +18,7 @@ jobs: persist-credentials: false - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v6 + uses: https://github.com/astral-sh/setup-uv@v5 with: enable-cache: true ignore-nothing-to-cache: true From 427b973b67ec256aaffc8c8c98dd49aef6fa73c5 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:51:02 +0200 Subject: [PATCH 04/17] chore(rust): bump version 1.87 -> 1.89 - bump version in rust-toolchain.toml - update sha in flake.nix --- flake.nix | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index f0dcb6fb..25629621 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ file = ./rust-toolchain.toml; # See also `rust-toolchain.toml` - sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; + sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; }; mkScope = diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bdb608aa..c44e95ef 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -9,8 +9,8 @@ # If you're having trouble making the relevant changes, bug a maintainer. [toolchain] -channel = "1.87.0" profile = "minimal" +channel = "1.89.0" components = [ # For rust-analyzer "rust-src", From ca3ee9224b19f7c2181a6db41c0de17b43317dc1 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:35:46 +0200 Subject: [PATCH 05/17] chore(rust): drop rustfmt from rust-toolchain.toml This just installs regular rustfmt, which is not needed in this project. One could say "It doesn't hurt", but in the NixOS dev shell it actually does since it will shadow nightly rustfmt and we don't have the `cargo +nightly fmt` synatx on NixOS that is available on other Distros. Also "It doesn't hurt" to delete it for non NixOS users. --- rust-toolchain.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c44e95ef..63e9d9ce 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -16,6 +16,9 @@ components = [ "rust-src", "rust-analyzer", # For CI and editors - "rustfmt", "clippy", + # you have to install rustfmt nightly yourself (if you're not on NixOS) + # + # The rust-toolchain.toml file doesn't provide any syntax for specifying components from different toolchains + # "rustfmt" ] From 6d1f12b22de7eb8d7af7a642cdf23a14c13d51d2 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:41:14 +0200 Subject: [PATCH 06/17] chore(nix): make rustfmt-nightly available to default dev shell I verified this by running `rustfmt --version` on my system. Note that I don't have a system-wide install of rust and only rely on dev shells, so this can't possibly come from somewhere else. ``` $ rustfmt --version rustfmt 1.8.0-nightly (6677875279 2025-07-02) ``` --- flake.nix | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 25629621..d6beb84e 100644 --- a/flake.nix +++ b/flake.nix @@ -31,13 +31,17 @@ inherit system; }; + fnx = inputs.fenix.packages.${system}; # The Rust toolchain to use - toolchain = inputs.fenix.packages.${system}.fromToolchainFile { - file = ./rust-toolchain.toml; + toolchain = fnx.combine [ + (fnx.fromToolchainFile { + file = ./rust-toolchain.toml; - # See also `rust-toolchain.toml` - sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; - }; + # See also `rust-toolchain.toml` + sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; + }) + fnx.complete.rustfmt + ]; mkScope = pkgs: From d191494f18fa60af15756fc01420b4823c6247bd Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:50:08 +0200 Subject: [PATCH 07/17] chore(nix): update `fenix` input This is required, since now we're installing `rustfmt` from the latest state of the fenix repo. This wasn't recent enough for the latest rust version. The input was locked at (2025-07-02). Now it's up to date. --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 4af82c75..4c2bf9fb 100644 --- a/flake.lock +++ b/flake.lock @@ -153,11 +153,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1751525020, - "narHash": "sha256-oDO6lCYS5Bf4jUITChj9XV7k3TP38DE0Ckz5n5ORCME=", + "lastModified": 1755585599, + "narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=", "owner": "nix-community", "repo": "fenix", - "rev": "a1a5f92f47787e7df9f30e5e5ac13e679215aa1e", + "rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42", "type": "github" }, "original": { @@ -546,11 +546,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1751433876, - "narHash": "sha256-IsdwOcvLLDDlkFNwhdD5BZy20okIQL01+UQ7Kxbqh8s=", + "lastModified": 1755504847, + "narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "11d45c881389dae90b0da5a94cde52c79d0fc7ef", + "rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75", "type": "github" }, "original": { From 8b35de6a430fae8be5d8291d8d73c9420aafff6a Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 22 Aug 2025 00:51:54 +0100 Subject: [PATCH 08/17] chore: Fix clippy lints with minimal diff --- Cargo.toml | 5 ++++- src/api/client/sync/v4.rs | 1 + src/core/config/mod.rs | 1 + src/core/debug.rs | 2 +- src/router/serve/unix.rs | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c656e183..04ff4bb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -867,7 +867,7 @@ unused-qualifications = "warn" #unused-results = "warn" # TODO ## some sadness -elided_named_lifetimes = "allow" # TODO! +mismatched_lifetime_syntaxes = "allow" # TODO! let_underscore_drop = "allow" missing_docs = "allow" # cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g. @@ -1006,3 +1006,6 @@ literal_string_with_formatting_args = { level = "allow", priority = 1 } needless_raw_string_hashes = "allow" + +# TODO: Enable this lint & fix all instances +collapsible_if = "allow" diff --git a/src/api/client/sync/v4.rs b/src/api/client/sync/v4.rs index 14cd50d8..a16e4526 100644 --- a/src/api/client/sync/v4.rs +++ b/src/api/client/sync/v4.rs @@ -45,6 +45,7 @@ use crate::{ type TodoRooms = BTreeMap, usize, u64)>; const SINGLE_CONNECTION_SYNC: &str = "single_connection_sync"; +#[allow(clippy::cognitive_complexity)] /// POST `/_matrix/client/unstable/org.matrix.msc3575/sync` /// /// Sliding Sync endpoint (future endpoint: `/_matrix/client/v4/sync`) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index aa021be7..0708196d 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::doc_link_with_quotes)] pub mod check; pub mod manager; pub mod proxy; diff --git a/src/core/debug.rs b/src/core/debug.rs index 21a5ada4..c728278d 100644 --- a/src/core/debug.rs +++ b/src/core/debug.rs @@ -100,7 +100,7 @@ pub fn trap() { #[must_use] pub fn panic_str(p: &Box) -> &'static str { - p.downcast_ref::<&str>().copied().unwrap_or_default() + (**p).downcast_ref::<&str>().copied().unwrap_or_default() } #[inline(always)] diff --git a/src/router/serve/unix.rs b/src/router/serve/unix.rs index 2af17274..9bb3dd6e 100644 --- a/src/router/serve/unix.rs +++ b/src/router/serve/unix.rs @@ -30,7 +30,7 @@ use tower::{Service, ServiceExt}; type MakeService = IntoMakeServiceWithConnectInfo; -const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); +const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); const FINI_POLL_INTERVAL: Duration = Duration::from_millis(750); #[tracing::instrument(skip_all, level = "debug")] From c7adbae03f72250d0fd0ab2ea6ab7911b4369467 Mon Sep 17 00:00:00 2001 From: RatCornu Date: Sat, 9 Aug 2025 15:06:48 +0200 Subject: [PATCH 09/17] feat: ldap login --- Cargo.lock | 413 +++++++++++++++++++----- Cargo.toml | 5 + conduwuit-example.toml | 91 ++++++ src/admin/user/commands.rs | 4 +- src/api/Cargo.toml | 3 + src/api/client/account.rs | 5 +- src/api/client/profile.rs | 6 +- src/api/client/session.rs | 211 ++++++++---- src/api/client/unstable.rs | 4 +- src/core/config/mod.rs | 108 +++++++ src/core/error/mod.rs | 2 + src/database/maps.rs | 4 + src/service/Cargo.toml | 5 + src/service/admin/create.rs | 2 +- src/service/emergency/mod.rs | 8 +- src/service/rooms/state_cache/update.rs | 2 +- src/service/users/mod.rs | 203 +++++++++++- 17 files changed, 921 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d7192b6..2b044a1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -183,7 +183,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_derive", - "syn", + "syn 2.0.104", ] [[package]] @@ -198,6 +198,45 @@ dependencies = [ "winnow", ] +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "assign" version = "1.1.1" @@ -250,7 +289,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -261,7 +300,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -433,11 +472,11 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rustls", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", ] @@ -452,9 +491,9 @@ dependencies = [ "http", "http-body-util", "pin-project", - "rustls", + "rustls 0.23.29", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower-layer", "tower-service", @@ -521,7 +560,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn", + "syn 2.0.104", "which", ] @@ -540,7 +579,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn", + "syn 2.0.104", ] [[package]] @@ -794,7 +833,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -972,7 +1011,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", - "ring", + "ring 0.17.14", "ruma", "sanitize-filename", "serde", @@ -1019,7 +1058,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1044,7 +1083,7 @@ dependencies = [ "hyper-util", "log", "ruma", - "rustls", + "rustls 0.23.29", "sd-notify", "sentry", "sentry-tower", @@ -1074,6 +1113,7 @@ dependencies = [ "image", "ipaddress", "itertools 0.14.0", + "ldap3", "log", "loole", "lru-cache", @@ -1183,6 +1223,16 @@ dependencies = [ "crossterm", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1256,7 +1306,7 @@ dependencies = [ "proc-macro2", "quote", "strict", - "syn", + "syn 2.0.104", ] [[package]] @@ -1366,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1393,7 +1443,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1434,6 +1484,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1461,7 +1525,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1483,7 +1547,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1544,7 +1608,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1564,7 +1628,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1734,6 +1798,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1781,7 +1846,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2030,7 +2095,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.9.2", - "ring", + "ring 0.17.14", "serde", "thiserror 2.0.12", "tinyvec", @@ -2122,7 +2187,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2216,11 +2281,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 1.0.2", ] @@ -2444,7 +2509,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2613,7 +2678,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 2.0.104", ] [[package]] @@ -2628,6 +2693,43 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "nom", + "percent-encoding", + "ring 0.16.20", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tokio-stream", + "tokio-util", + "url", + "x509-parser", +] + [[package]] name = "lebe" version = "0.5.2" @@ -2866,7 +2968,7 @@ checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3032,7 +3134,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3094,6 +3196,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3284,7 +3395,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3358,7 +3469,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3458,7 +3569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.104", ] [[package]] @@ -3487,7 +3598,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "version_check", "yansi", ] @@ -3508,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3531,7 +3642,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3597,7 +3708,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "socket2", "thiserror 2.0.12", "tokio", @@ -3615,9 +3726,9 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.2", - "ring", + "ring 0.17.14", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -3876,16 +3987,16 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tower 0.5.2", @@ -3909,6 +4020,21 @@ version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -3919,7 +4045,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4093,7 +4219,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn", + "syn 2.0.104", "toml", ] @@ -4178,6 +4304,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4204,6 +4339,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.29" @@ -4213,13 +4360,25 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4229,7 +4388,16 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -4251,6 +4419,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.103.4" @@ -4258,9 +4436,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4319,6 +4497,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "sd-notify" version = "0.4.5" @@ -4328,6 +4516,19 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -4335,7 +4536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4365,7 +4566,7 @@ checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335" dependencies = [ "httpdate", "reqwest", - "rustls", + "rustls 0.23.29", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -4509,7 +4710,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4723,6 +4924,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spki" version = "0.7.3" @@ -4791,6 +4998,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.104" @@ -4811,6 +5029,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4819,7 +5049,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4910,7 +5140,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4921,7 +5151,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5088,7 +5318,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5103,13 +5333,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.29", "tokio", ] @@ -5307,7 +5547,7 @@ source = "git+https://forgejo.ellis.link/continuwuation/tracing?rev=1e64095a8051 dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5461,12 +5701,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5482,7 +5734,7 @@ dependencies = [ "base64 0.22.1", "log", "once_cell", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -5617,7 +5869,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -5652,7 +5904,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5832,7 +6084,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5843,7 +6095,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6164,6 +6416,23 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xml5ever" version = "0.18.1" @@ -6221,8 +6490,8 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6242,7 +6511,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6262,8 +6531,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6302,7 +6571,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 04ff4bb7..9452066c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -546,6 +546,11 @@ features = ["std"] [workspace.dependencies.maplit] version = "1.0.2" +[workspace.dependencies.ldap3] +version = "0.11.5" +default-features = false +features = ["sync", "tls-rustls"] + # # Patches # diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 541050b1..47a9da19 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1696,6 +1696,10 @@ # #config_reload_signal = true +# This item is undocumented. Please contribute documentation for it. +# +#ldap = false + [global.tls] # Path to a valid TLS certificate file. @@ -1774,3 +1778,90 @@ # is 33.55MB. Setting it to 0 disables blurhashing. # #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 = + +# Root of the searches. +# +# example: "ou=users,dc=example,dc=org" +# +#base_dn = false + +# 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 = 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. +# +# 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 mail of the user. +# +# example: "mail" +# +#mail_attribute = "mail" + +# Attribute containing the distinguished 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 = false + +# The LDAP search filter to find administrative users for conduwuit. +# +# 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 = false diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 86206c2b..56864a32 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -68,7 +68,8 @@ pub(super) async fn create_user(&self, username: String, password: Option return Err!("Couldn't reset the password for user {user_id}: {e}"), | Ok(()) => { diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 15ada812..9b4ea460 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -49,6 +49,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", diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 0cea7bd9..67268c9f 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -373,7 +373,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)?; + services.users.create(&user_id, password, None).await?; // Default to pretty displayname let mut displayname = user_id.localpart().to_owned(); @@ -659,7 +659,8 @@ pub(crate) async fn change_password_route( services .users - .set_password(sender_user, Some(&body.new_password))?; + .set_password(sender_user, Some(&body.new_password)) + .await?; if body.logout_devices { // Logout all devices except the current one diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 1882495c..eaa66e70 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -90,7 +90,7 @@ pub(crate) async fn get_displayname_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -189,7 +189,7 @@ pub(crate) async fn get_avatar_url_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -248,7 +248,7 @@ pub(crate) async fn get_profile_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/api/client/session.rs b/src/api/client/session.rs index fe07e41d..2f066d58 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -6,10 +6,11 @@ use conduwuit::{ Err, Error, Result, debug, err, info, utils, utils::{ReadyExt, hash}, }; -use conduwuit_service::uiaa::SESSION_ID_LENGTH; +use conduwuit_core::debug_error; +use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH}; use futures::StreamExt; use ruma::{ - UserId, + OwnedUserId, UserId, api::client::{ session::{ get_login_token, @@ -49,6 +50,147 @@ 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. +#[tracing::instrument(skip_all, fields(%user_id), name = "password")] +pub(crate) async fn password_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + // 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 + .users + .password_hash(lowercased_user_id) + .await + .map(|hash| (hash, lowercased_user_id)) + .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?, + }; + + 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. +#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")] +pub(super) async fn ldap_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + 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()), false), + | _ => { + 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?; + } + + let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await; + + if is_ldap_admin && !is_conduwuit_admin { + services.admin.make_user_admin(lowercased_user_id).await?; + } else if !is_ldap_admin && is_conduwuit_admin { + services.admin.revoke_admin(lowercased_user_id).await?; + } + + Ok(user_id) +} + +pub(crate) async fn handle_login( + services: &Services, + body: &Ruma, + identifier: &Option, + password: &str, + user: &Option, +) -> Result { + debug!("Got password login type"); + let user_id = + if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id, &services.config.server_name) + } else if let Some(user) = user { + UserId::parse_with_server_name(user, &services.config.server_name) + } else { + return Err!(Request(Unknown( + debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") + ))); + } + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + let lowercased_user_id = UserId::parse_with_server_name( + user_id.localpart().to_lowercase(), + &services.config.server_name, + )?; + + if !services.globals.user_is_local(&user_id) + || !services.globals.user_is_local(&lowercased_user_id) + { + return Err!(Request(Unknown("User ID does not belong to this homeserver"))); + } + + if cfg!(feature = "ldap") && services.config.ldap.enable { + ldap_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` /// /// Authenticates the user and returns an access token it can use in subsequent @@ -80,70 +222,7 @@ pub(crate) async fn login_route( password, user, .. - }) => { - debug!("Got password login type"); - let user_id = - if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - UserId::parse_with_server_name(user_id, &services.config.server_name) - } else if let Some(user) = user { - UserId::parse_with_server_name(user, &services.config.server_name) - } else { - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") - ))); - } - .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; - - let lowercased_user_id = UserId::parse_with_server_name( - user_id.localpart().to_lowercase(), - &services.config.server_name, - )?; - - if !services.globals.user_is_local(&user_id) - || !services.globals.user_is_local(&lowercased_user_id) - { - return Err!(Request(Unknown("User ID does not belong to this homeserver"))); - } - - // first try the username as-is - let hash = services - .users - .password_hash(&user_id) - .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 - }, - } - }, + }) => handle_login(&services, &body, identifier, password, user).await?, | login::v3::LoginInfo::Token(login::v3::Token { token }) => { debug!("Got token login type"); if !services.server.config.login_via_existing_session { diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index 08f70975..f8703ff3 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -292,7 +292,7 @@ pub(crate) async fn get_timezone_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -352,7 +352,7 @@ pub(crate) async fn get_profile_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 0708196d..a7fbc7dc 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1948,6 +1948,10 @@ pub struct Config { pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure: bool, + // external structure; separate section + #[serde(default)] + pub ldap: LdapConfig, + // external structure; separate section #[serde(default)] pub blurhashing: BlurhashConfig, @@ -2042,6 +2046,102 @@ pub struct BlurhashConfig { pub blurhash_max_raw_size: u64, } +#[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, + + /// URI of the LDAP server. + /// + /// example: "ldap://ldap.example.com:389" + pub uri: Option, + + /// 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. + /// + /// 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" + #[serde(default)] + pub bind_dn: Option, + + /// 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, + + /// 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 mail of the user. + /// + /// example: "mail" + /// + /// default: "mail" + #[serde(default = "default_ldap_mail_attribute")] + pub mail_attribute: String, + + /// Attribute containing the distinguished 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" + #[serde(default)] + pub admin_base_dn: String, + + /// The LDAP search filter to find administrative users for conduwuit. + /// + /// 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})" + #[serde(default)] + pub admin_filter: String, +} + #[derive(Deserialize, Clone, Debug)] #[serde(transparent)] struct ListeningPort { @@ -2431,3 +2531,11 @@ 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_mail_attribute() -> String { String::from("mail") } + +fn default_ldap_name_attribute() -> String { String::from("givenName") } diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index e46edf09..541af793 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -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)] diff --git a/src/database/maps.rs b/src/database/maps.rs index 214dbf34..da97ef45 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -374,6 +374,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userid_masterkeyid", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "userid_origin", + ..descriptor::RANDOM + }, Descriptor { name: "userid_password", ..descriptor::RANDOM diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index fdebd1d7..6e538f40 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -53,6 +53,9 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-database/jemalloc_stats", ] +ldap = [ + "dep:ldap3" +] media_thumbnail = [ "dep:image", ] @@ -89,6 +92,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 diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 157b4d65..755673fe 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -38,7 +38,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)?; + services.users.create(server_user, None, None).await?; let create_content = { use RoomVersionId::*; diff --git a/src/service/emergency/mod.rs b/src/service/emergency/mod.rs index 3a61f710..f8ecbb3e 100644 --- a/src/service/emergency/mod.rs +++ b/src/service/emergency/mod.rs @@ -41,6 +41,11 @@ impl crate::Service for Service { return Ok(()); } + 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}"); }) @@ -57,7 +62,8 @@ impl Service { self.services .users - .set_password(server_user, self.services.config.emergency_password.as_deref())?; + .set_password(server_user, self.services.config.emergency_password.as_deref()) + .await?; let (ruleset, pwd_set) = match self.services.config.emergency_password { | Some(_) => (Ruleset::server_default(server_user), true), diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 32c67947..86c1afe7 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -49,7 +49,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)?; + self.services.users.create(user_id, None, None).await?; } } diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index eb54660e..35202ec7 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,11 +1,19 @@ -use std::{collections::BTreeMap, mem, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + mem, + sync::Arc, +}; use conduwuit::{ - Err, Error, Result, Server, at, debug_warn, err, trace, + Err, Error, Result, Server, at, debug_warn, err, is_equal_to, + result::LogErr, + trace, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, }; +use conduwuit_core::{debug, error}; use database::{Deserialized, Ignore, Interfix, Json, Map}; use futures::{Stream, StreamExt, TryFutureExt}; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId, @@ -63,6 +71,7 @@ struct Data { userid_displayname: Arc, userid_lastonetimekeyupdate: Arc, userid_masterkeyid: Arc, + userid_origin: Arc, userid_password: Arc, userid_suspension: Arc, userid_selfsigningkeyid: Arc, @@ -100,6 +109,7 @@ 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_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), @@ -136,9 +146,21 @@ 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 fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { - self.set_password(user_id, password) + pub async fn create( + &self, + user_id: &UserId, + password: Option<&str>, + origin: Option<&str>, + ) -> Result<()> { + self.db + .userid_origin + .insert(user_id, origin.unwrap_or("password")); + self.set_password(user_id, password).await } /// Deactivate account @@ -152,7 +174,7 @@ impl Service { // result in an empty string, so the user will not be able to log in again. // Systems like changing the password without logging in should check if the // account is deactivated. - self.set_password(user_id, None)?; + self.set_password(user_id, None).await?; // TODO: Unhook 3PID Ok(()) @@ -253,13 +275,34 @@ impl Service { .ready_filter_map(|(u, p): (&UserId, &[u8])| (!p.is_empty()).then_some(u)) } + /// Returns the origin of the user (password/LDAP/...). + pub async fn origin(&self, user_id: &UserId) -> Result { + 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 { self.db.userid_password.get(user_id).await.deserialized() } /// Hash and set the user's password to the Argon2 hash - pub 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 cfg!(feature = "ldap") + && password.is_some_and(|pwd| pwd != "*") + && self + .db + .userid_origin + .get(user_id) + .await + .deserialized::() + .is_ok_and(is_equal_to!("ldap")) + { + return Err!(Request(InvalidParam("Cannot change password of a LDAP user"))); + } + password .map(utils::hash::password) .transpose() @@ -1132,6 +1175,154 @@ impl Service { self.db.useridprofilekey_value.del(key); } } + + #[cfg(not(feature = "ldap"))] + pub async fn search_ldap(&self, _user_id: &UserId) -> Result> { + Err!(FeatureDisabled("ldap")) + } + + #[cfg(feature = "ldap")] + pub async fn search_ldap(&self, user_id: &UserId) -> Result> { + 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) = LdapConnAsync::new(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 = 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, false)) + }) + .collect(); + + if !config.admin_filter.is_empty() { + 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, 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) = LdapConnAsync::new(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( From fb7e739b72dad43211ff9b6077c33c479e2574f5 Mon Sep 17 00:00:00 2001 From: RatCornu Date: Sun, 10 Aug 2025 12:50:19 +0200 Subject: [PATCH 10/17] chore: remove unused LDAP mail attribute --- conduwuit-example.toml | 18 ++++++------------ src/api/client/session.rs | 8 ++++---- src/core/config/mod.rs | 26 ++++++++++---------------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 47a9da19..68f5b965 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1797,7 +1797,7 @@ # # example: "ou=users,dc=example,dc=org" # -#base_dn = false +#base_dn = # Bind DN if anonymous search is not enabled. # @@ -1810,7 +1810,7 @@ # example: "cn=ldap-reader,dc=example,dc=org" or # "cn={username},ou=users,dc=example,dc=org" # -#bind_dn = false +#bind_dn = # Path to a file on the system that contains the password for the # `bind_dn`. @@ -1834,13 +1834,7 @@ # #uid_attribute = "uid" -# Attribute containing the mail of the user. -# -# example: "mail" -# -#mail_attribute = "mail" - -# Attribute containing the distinguished name of the user. +# Attribute containing the display name of the user. # # example: "givenName" or "sn" # @@ -1852,9 +1846,9 @@ # # example: "ou=admins,dc=example,dc=org" # -#admin_base_dn = false +#admin_base_dn = -# The LDAP search filter to find administrative users for conduwuit. +# The LDAP search filter to find administrative users for continuwuity. # # If left blank, administrative state must be configured manually for each # user. @@ -1864,4 +1858,4 @@ # # example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # -#admin_filter = false +#admin_filter = diff --git a/src/api/client/session.rs b/src/api/client/session.rs index 2f066d58..c57f5487 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -156,9 +156,9 @@ pub(super) async fn ldap_login( pub(crate) async fn handle_login( services: &Services, body: &Ruma, - identifier: &Option, + identifier: Option<&uiaa::UserIdentifier>, password: &str, - user: &Option, + user: Option<&String>, ) -> Result { debug!("Got password login type"); let user_id = @@ -185,7 +185,7 @@ pub(crate) async fn handle_login( } if cfg!(feature = "ldap") && services.config.ldap.enable { - ldap_login(services, &user_id, &lowercased_user_id, password).await + Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await } else { password_login(services, &user_id, &lowercased_user_id, password).await } @@ -222,7 +222,7 @@ pub(crate) async fn login_route( password, user, .. - }) => handle_login(&services, &body, identifier, password, user).await?, + }) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?, | login::v3::LoginInfo::Token(login::v3::Token { token }) => { debug!("Got token login type"); if !services.server.config.login_via_existing_session { diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index a7fbc7dc..e996d1fa 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2063,7 +2063,7 @@ pub struct LdapConfig { /// Root of the searches. /// /// example: "ou=users,dc=example,dc=org" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub base_dn: String, /// Bind DN if anonymous search is not enabled. @@ -2076,7 +2076,7 @@ pub struct LdapConfig { /// /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" - #[serde(default)] + #[serde(default = "some_empty_string_fn")] pub bind_dn: Option, /// Path to a file on the system that contains the password for the @@ -2105,15 +2105,7 @@ pub struct LdapConfig { #[serde(default = "default_ldap_uid_attribute")] pub uid_attribute: String, - /// Attribute containing the mail of the user. - /// - /// example: "mail" - /// - /// default: "mail" - #[serde(default = "default_ldap_mail_attribute")] - pub mail_attribute: String, - - /// Attribute containing the distinguished name of the user. + /// Attribute containing the display name of the user. /// /// example: "givenName" or "sn" /// @@ -2126,10 +2118,10 @@ pub struct LdapConfig { /// Defaults to `base_dn` if empty. /// /// example: "ou=admins,dc=example,dc=org" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub admin_base_dn: String, - /// The LDAP search filter to find administrative users for conduwuit. + /// The LDAP search filter to find administrative users for continuwuity. /// /// If left blank, administrative state must be configured manually for each /// user. @@ -2138,7 +2130,7 @@ pub struct LdapConfig { /// entered username for more complex filters. /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub admin_filter: String, } @@ -2240,6 +2232,10 @@ impl Config { fn true_fn() -> bool { true } +fn empty_string_fn() -> String { String::new() } + +fn some_empty_string_fn() -> Option { Some(String::new()) } + fn default_address() -> ListeningAddr { ListeningAddr { addrs: Right(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]), @@ -2536,6 +2532,4 @@ 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("givenName") } From c58b9f05ed89e8a3c4d5d080afa05a3c1aa48882 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 20:49:08 +0100 Subject: [PATCH 11/17] chore: Fix default attributes for config --- conduwuit-example.toml | 4 ++-- src/core/config/mod.rs | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 68f5b965..06e67a89 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1797,7 +1797,7 @@ # # example: "ou=users,dc=example,dc=org" # -#base_dn = +#base_dn = "" # Bind DN if anonymous search is not enabled. # @@ -1846,7 +1846,7 @@ # # example: "ou=admins,dc=example,dc=org" # -#admin_base_dn = +#admin_base_dn = "" # The LDAP search filter to find administrative users for continuwuity. # diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index e996d1fa..13778b5e 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2063,7 +2063,9 @@ pub struct LdapConfig { /// Root of the searches. /// /// example: "ou=users,dc=example,dc=org" - #[serde(default = "empty_string_fn")] + /// + /// default: "" + #[serde(default)] pub base_dn: String, /// Bind DN if anonymous search is not enabled. @@ -2076,7 +2078,9 @@ pub struct LdapConfig { /// /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" - #[serde(default = "some_empty_string_fn")] + /// + /// default: + #[serde(default)] pub bind_dn: Option, /// Path to a file on the system that contains the password for the @@ -2118,7 +2122,9 @@ pub struct LdapConfig { /// Defaults to `base_dn` if empty. /// /// example: "ou=admins,dc=example,dc=org" - #[serde(default = "empty_string_fn")] + /// + /// default: "" + #[serde(default)] pub admin_base_dn: String, /// The LDAP search filter to find administrative users for continuwuity. @@ -2130,7 +2136,9 @@ pub struct LdapConfig { /// entered username for more complex filters. /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" - #[serde(default = "empty_string_fn")] + /// + /// default: + #[serde(default)] pub admin_filter: String, } @@ -2232,10 +2240,6 @@ impl Config { fn true_fn() -> bool { true } -fn empty_string_fn() -> String { String::new() } - -fn some_empty_string_fn() -> Option { Some(String::new()) } - fn default_address() -> ListeningAddr { ListeningAddr { addrs: Right(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]), From 0ed691edef9d95c8a2e3b54ba8b72ecb6a326e88 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 20:54:05 +0100 Subject: [PATCH 12/17] fix: Make builds without LDAP work correctly --- src/service/users/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 35202ec7..fff1661c 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,18 +1,18 @@ -use std::{ - collections::{BTreeMap, HashMap}, - mem, - sync::Arc, -}; +#[cfg(feature = "ldap")] +use std::collections::HashMap; +use std::{collections::BTreeMap, mem, sync::Arc}; +#[cfg(feature = "ldap")] +use conduwuit::result::LogErr; use conduwuit::{ - Err, Error, Result, Server, at, debug_warn, err, is_equal_to, - result::LogErr, - trace, + Err, Error, Result, Server, at, 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, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, From cb09bfa4e7be034164f810d857fc505d692c06d7 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 21:08:06 +0100 Subject: [PATCH 13/17] fix: Correctly pass ldap feature from the default crate --- src/main/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index 8aaa3cc6..eafa1e48 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -56,6 +56,7 @@ standard = [ "jemalloc", "jemalloc_conf", "journald", + "ldap", "media_thumbnail", "systemd", "url_preview", @@ -114,6 +115,9 @@ jemalloc_stats = [ jemalloc_conf = [ "conduwuit-core/jemalloc_conf", ] +ldap = [ + "conduwuit-api/ldap", +] media_thumbnail = [ "conduwuit-service/media_thumbnail", ] From 57d77430370616bbc40aac145da1b79d516431ce Mon Sep 17 00:00:00 2001 From: RatCornu Date: Thu, 14 Aug 2025 22:48:55 +0200 Subject: [PATCH 14/17] feat: add ldap_only config option --- conduwuit-example.toml | 14 ++++++++++---- src/api/client/session.rs | 15 +++++++++++---- src/core/config/mod.rs | 16 ++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 06e67a89..41fbfb3a 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1787,11 +1787,17 @@ # #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 = +#uri = "" # Root of the searches. # @@ -1810,14 +1816,14 @@ # example: "cn=ldap-reader,dc=example,dc=org" or # "cn={username},ou=users,dc=example,dc=org" # -#bind_dn = +#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 = false +#bind_password_file = "" # Search filter to limit user searches. # @@ -1858,4 +1864,4 @@ # # example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # -#admin_filter = +#admin_filter = "" diff --git a/src/api/client/session.rs b/src/api/client/session.rs index c57f5487..da7bed2c 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -3,10 +3,10 @@ use std::time::Duration; use axum::extract::State; use axum_client_ip::InsecureClientIp; use conduwuit::{ - Err, Error, Result, debug, err, info, utils, - utils::{ReadyExt, hash}, + Err, Error, Result, debug, err, info, + utils::{self, ReadyExt, hash}, }; -use conduwuit_core::debug_error; +use conduwuit_core::{debug_error, debug_warn}; use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH}; use futures::StreamExt; use ruma::{ @@ -185,7 +185,14 @@ pub(crate) async fn handle_login( } if cfg!(feature = "ldap") && services.config.ldap.enable { - Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await + 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 } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 13778b5e..e8518ed4 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2055,9 +2055,19 @@ pub struct LdapConfig { #[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, /// Root of the searches. @@ -2079,7 +2089,7 @@ pub struct LdapConfig { /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" /// - /// default: + /// default: "" #[serde(default)] pub bind_dn: Option, @@ -2087,6 +2097,8 @@ pub struct LdapConfig { /// `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, @@ -2137,7 +2149,7 @@ pub struct LdapConfig { /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" /// - /// default: + /// default: "" #[serde(default)] pub admin_filter: String, } From 3183210459feb640734341a189a8437fdf7f2240 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 23 Aug 2025 21:28:31 +0100 Subject: [PATCH 15/17] fix: Post-merge compile issues --- conduwuit-example.toml | 3 ++- src/service/appservice/mod.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 41fbfb3a..f0e510b4 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1787,7 +1787,8 @@ # #enable = false -# Whether to force LDAP authentication or authorize classical password login. +# Whether to force LDAP authentication or authorize classical password +# login. # # example: "true" # diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index ebd798f6..adbf3b6e 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -109,7 +109,10 @@ impl Service { )?; if !self.services.users.exists(&appservice_user_id).await { - self.services.users.create(&appservice_user_id, None)?; + self.services + .users + .create(&appservice_user_id, None, None) + .await?; } else if self .services .users @@ -120,7 +123,8 @@ impl Service { // Reactivate the appservice user if it was accidentally deactivated self.services .users - .set_password(&appservice_user_id, None)?; + .set_password(&appservice_user_id, None) + .await?; } self.registration_info From 30a56d5cb95b2545000088d38855724a5a956b40 Mon Sep 17 00:00:00 2001 From: nex Date: Thu, 28 Aug 2025 17:15:32 +0000 Subject: [PATCH 16/17] Update renovate.json --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index b48fc6dc..deb428af 100644 --- a/renovate.json +++ b/renovate.json @@ -13,8 +13,8 @@ "enabled": true }, "labels": [ - "dependencies", - "github_actions" + "Dependencies", + "Dependencies/Renovate" ], "ignoreDeps": [ "tikv-jemallocator", From dd22325ea2676c30f784c989e025e0391dbe911c Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Mon, 18 Aug 2025 20:45:30 +0100 Subject: [PATCH 17/17] refactor(ci): Consolidate Rust checks with optimised toolchain setup Merge rust-checks.yml into prek-checks.yml for a unified workflow that runs formatting and clippy/test checks in parallel jobs. Add reusable composite actions: - setup-rust: Smart Rust toolchain management with caching * Uses cargo-binstall for pre-built binary downloads * Integrates Mozilla sccache-action for compilation caching * Workspace-relative paths for better cache control * GitHub token support for improved rate limits - setup-llvm-with-apt: LLVM installation with native dependencies - detect-runner-os: Consistent OS detection for cache keys Key improvements: - Install prek via cargo-binstall --git (crates.io outdated at v0.0.1) - Download timelord-cli from cargo-quickinstall - Set BINSTALL_MAXIMUM_RESOLUTION_TIMEOUT=10 to avoid rate limit delays - Default Rust version 1.87.0 with override support - Remove redundant sccache stats (handled by Mozilla action) Significantly reduces CI runtime through binary downloads instead of compilation while maintaining all existing quality checks. --- .forgejo/actions/detect-runner-os/action.yml | 39 +++ .../actions/setup-llvm-with-apt/action.yml | 167 +++++++++++++ .forgejo/actions/setup-rust/action.yml | 236 ++++++++++++++++++ .forgejo/workflows/prek-checks.yml | 59 ++++- .forgejo/workflows/rust-checks.yml | 144 ----------- 5 files changed, 494 insertions(+), 151 deletions(-) create mode 100644 .forgejo/actions/detect-runner-os/action.yml create mode 100644 .forgejo/actions/setup-llvm-with-apt/action.yml create mode 100644 .forgejo/actions/setup-rust/action.yml delete mode 100644 .forgejo/workflows/rust-checks.yml diff --git a/.forgejo/actions/detect-runner-os/action.yml b/.forgejo/actions/detect-runner-os/action.yml new file mode 100644 index 00000000..6ada1d5d --- /dev/null +++ b/.forgejo/actions/detect-runner-os/action.yml @@ -0,0 +1,39 @@ +name: detect-runner-os +description: | + Detect the actual OS name and version of the runner. + Provides separate outputs for name, version, and a combined slug. + +outputs: + name: + description: 'OS name (e.g. Ubuntu, Debian)' + value: ${{ steps.detect.outputs.name }} + version: + description: 'OS version (e.g. 22.04, 11)' + value: ${{ steps.detect.outputs.version }} + slug: + description: 'Combined OS slug (e.g. Ubuntu-22.04)' + value: ${{ steps.detect.outputs.slug }} + +runs: + using: composite + steps: + - name: Detect runner OS + id: detect + shell: bash + run: | + # Detect OS version (try lsb_release first, fall back to /etc/os-release) + OS_VERSION=$(lsb_release -rs 2>/dev/null || grep VERSION_ID /etc/os-release | cut -d'"' -f2) + + # Detect OS name and capitalise (try lsb_release first, fall back to /etc/os-release) + OS_NAME=$(lsb_release -is 2>/dev/null || grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"' | sed 's/\b\(.\)/\u\1/g') + + # Create combined slug + OS_SLUG="${OS_NAME}-${OS_VERSION}" + + # Set outputs + echo "name=${OS_NAME}" >> $GITHUB_OUTPUT + echo "version=${OS_VERSION}" >> $GITHUB_OUTPUT + echo "slug=${OS_SLUG}" >> $GITHUB_OUTPUT + + # Log detection results + echo "🔍 Detected Runner OS: ${OS_NAME} ${OS_VERSION}" diff --git a/.forgejo/actions/setup-llvm-with-apt/action.yml b/.forgejo/actions/setup-llvm-with-apt/action.yml new file mode 100644 index 00000000..eb421e4f --- /dev/null +++ b/.forgejo/actions/setup-llvm-with-apt/action.yml @@ -0,0 +1,167 @@ +name: setup-llvm-with-apt +description: | + Set up LLVM toolchain with APT package management and smart caching. + Supports cross-compilation architectures and additional package installation. + + Creates symlinks in /usr/bin: clang, clang++, lld, llvm-ar, llvm-ranlib + +inputs: + dpkg-arch: + description: 'Debian architecture for cross-compilation (e.g. arm64)' + required: false + default: '' + extra-packages: + description: 'Additional APT packages to install (space-separated)' + required: false + default: '' + llvm-version: + description: 'LLVM version to install' + required: false + default: '20' + +outputs: + llvm-version: + description: 'Installed LLVM version' + value: ${{ steps.configure.outputs.version }} + +runs: + using: composite + steps: + - name: Detect runner OS + id: runner-os + uses: ./.forgejo/actions/detect-runner-os + + - name: Configure cross-compilation architecture + if: inputs.dpkg-arch != '' + shell: bash + run: | + echo "🏗️ Adding ${{ inputs.dpkg-arch }} architecture" + sudo dpkg --add-architecture ${{ inputs.dpkg-arch }} + + # Restrict default sources to amd64 + sudo sed -i 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list + sudo sed -i 's/^deb https/deb [arch=amd64] https/g' /etc/apt/sources.list + + # Add ports sources for foreign architecture + sudo tee /etc/apt/sources.list.d/${{ inputs.dpkg-arch }}.list > /dev/null <> $GITHUB_OUTPUT + else + echo "📦 LLVM ${{ inputs.llvm-version }} not found or incomplete - installing..." + + echo "::group::🔧 Installing LLVM ${{ inputs.llvm-version }}" + wget -O - https://apt.llvm.org/llvm.sh | bash -s -- ${{ inputs.llvm-version }} + echo "::endgroup::" + + if [ ! -f "/usr/bin/clang-${{ inputs.llvm-version }}" ]; then + echo "❌ Failed to install LLVM ${{ inputs.llvm-version }}" + exit 1 + fi + + echo "✅ Installed LLVM ${{ inputs.llvm-version }}" + echo "needs-install=true" >> $GITHUB_OUTPUT + fi + + - name: Prepare for additional packages + if: inputs.extra-packages != '' + shell: bash + run: | + # Update APT if LLVM was cached (installer script already does apt-get update) + if [[ "${{ steps.llvm-setup.outputs.needs-install }}" != "true" ]]; then + echo "::group::📦 Running apt-get update (LLVM cached, extra packages needed)" + sudo apt-get update + echo "::endgroup::" + fi + echo "::group::📦 Installing additional packages" + + - name: Install additional packages + if: inputs.extra-packages != '' + uses: https://github.com/awalsh128/cache-apt-pkgs-action@latest + with: + packages: ${{ inputs.extra-packages }} + version: 1.0 + + - name: End package installation group + if: inputs.extra-packages != '' + shell: bash + run: echo "::endgroup::" + + - name: Configure LLVM environment + id: configure + shell: bash + run: | + echo "::group::🔧 Configuring LLVM ${{ inputs.llvm-version }} environment" + + # Create symlinks + sudo ln -sf "/usr/bin/clang-${{ inputs.llvm-version }}" /usr/bin/clang + sudo ln -sf "/usr/bin/clang++-${{ inputs.llvm-version }}" /usr/bin/clang++ + sudo ln -sf "/usr/bin/lld-${{ inputs.llvm-version }}" /usr/bin/lld + sudo ln -sf "/usr/bin/llvm-ar-${{ inputs.llvm-version }}" /usr/bin/llvm-ar + sudo ln -sf "/usr/bin/llvm-ranlib-${{ inputs.llvm-version }}" /usr/bin/llvm-ranlib + echo " ✓ Created symlinks" + + # Setup library paths + LLVM_LIB_PATH="/usr/lib/llvm-${{ inputs.llvm-version }}/lib" + if [ -d "$LLVM_LIB_PATH" ]; then + echo "LD_LIBRARY_PATH=${LLVM_LIB_PATH}:${LD_LIBRARY_PATH:-}" >> $GITHUB_ENV + echo "LIBCLANG_PATH=${LLVM_LIB_PATH}" >> $GITHUB_ENV + + echo "$LLVM_LIB_PATH" | sudo tee "/etc/ld.so.conf.d/llvm-${{ inputs.llvm-version }}.conf" > /dev/null + sudo ldconfig + echo " ✓ Configured library paths" + else + # Fallback to standard library location + if [ -d "/usr/lib/x86_64-linux-gnu" ]; then + echo "LIBCLANG_PATH=/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV + echo " ✓ Using fallback library path" + fi + fi + + # Set output + echo "version=${{ inputs.llvm-version }}" >> $GITHUB_OUTPUT + echo "::endgroup::" + echo "✅ LLVM ready: $(clang --version | head -1)" diff --git a/.forgejo/actions/setup-rust/action.yml b/.forgejo/actions/setup-rust/action.yml new file mode 100644 index 00000000..091da8c2 --- /dev/null +++ b/.forgejo/actions/setup-rust/action.yml @@ -0,0 +1,236 @@ +name: setup-rust +description: | + Set up Rust toolchain with sccache for compilation caching. + Respects rust-toolchain.toml by default or accepts explicit version override. + +inputs: + cache-key-suffix: + description: 'Optional suffix for cache keys (e.g. platform identifier)' + required: false + default: '' + rust-components: + description: 'Additional Rust components to install (space-separated)' + required: false + default: '' + rust-target: + description: 'Rust target triple (e.g. x86_64-unknown-linux-gnu)' + required: false + default: '' + rust-version: + description: 'Rust version to install (e.g. nightly). Defaults to 1.87.0' + required: false + default: '1.87.0' + sccache-cache-limit: + description: 'Maximum size limit for sccache local cache (e.g. 2G, 500M)' + required: false + default: '2G' + github-token: + description: 'GitHub token for downloading sccache from GitHub releases' + required: false + default: '' + +outputs: + rust-version: + description: 'Installed Rust version' + value: ${{ steps.rust-setup.outputs.version }} + +runs: + using: composite + steps: + - name: Detect runner OS + id: runner-os + uses: ./.forgejo/actions/detect-runner-os + + - name: Configure Cargo environment + shell: bash + run: | + # Use workspace-relative paths for better control and consistency + echo "CARGO_HOME=${{ github.workspace }}/.cargo" >> $GITHUB_ENV + echo "CARGO_TARGET_DIR=${{ github.workspace }}/target" >> $GITHUB_ENV + echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> $GITHUB_ENV + echo "RUSTUP_HOME=${{ github.workspace }}/.rustup" >> $GITHUB_ENV + + # Limit binstall resolution timeout to avoid GitHub rate limit delays + echo "BINSTALL_MAXIMUM_RESOLUTION_TIMEOUT=10" >> $GITHUB_ENV + + # Ensure directories exist for first run + mkdir -p "${{ github.workspace }}/.cargo" + mkdir -p "${{ github.workspace }}/.sccache" + mkdir -p "${{ github.workspace }}/target" + mkdir -p "${{ github.workspace }}/.rustup" + + - name: Start cache restore group + shell: bash + run: echo "::group::📦 Restoring caches (registry, toolchain, build artifacts)" + + - name: Cache Cargo registry and git + id: registry-cache + uses: https://github.com/actions/cache@v4 + with: + path: | + .cargo/registry/index + .cargo/registry/cache + .cargo/git/db + # Registry cache saved per workflow, restored from any workflow's cache + # Each workflow maintains its own registry that accumulates its needed crates + key: cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ github.workflow }} + restore-keys: | + cargo-registry-${{ steps.runner-os.outputs.slug }}- + + - name: Cache toolchain binaries + id: toolchain-cache + uses: https://github.com/actions/cache@v4 + with: + path: | + .cargo/bin + .rustup/toolchains + .rustup/update-hashes + # Shared toolchain cache across all Rust versions + key: toolchain-${{ steps.runner-os.outputs.slug }} + + - name: Debug GitHub token availability + shell: bash + run: | + if [ -z "${{ inputs.github-token }}" ]; then + echo "⚠️ No GitHub token provided - sccache will use fallback download method" + else + echo "✅ GitHub token provided for sccache" + fi + + - name: Setup sccache + uses: https://github.com/mozilla-actions/sccache-action@v0.0.9 + with: + token: ${{ inputs.github-token }} + + - name: Cache build artifacts + id: build-cache + uses: https://github.com/actions/cache@v4 + with: + path: | + target/**/deps + !target/**/deps/*.rlib + target/**/build + target/**/.fingerprint + target/**/incremental + target/**/*.d + /timelord/ + # Build artifacts - cache per code change, restore from deps when code changes + key: >- + build-${{ steps.runner-os.outputs.slug }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }} + restore-keys: | + build-${{ steps.runner-os.outputs.slug }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}- + + - name: End cache restore group + shell: bash + run: echo "::endgroup::" + + - name: Setup Rust toolchain + shell: bash + run: | + # Install rustup if not already cached + if ! command -v rustup &> /dev/null; then + echo "::group::📦 Installing rustup" + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain none + source "$CARGO_HOME/env" + echo "::endgroup::" + else + echo "✅ rustup already available" + fi + + # Setup the appropriate Rust version + if [[ -n "${{ inputs.rust-version }}" ]]; then + echo "::group::📦 Setting up Rust ${{ inputs.rust-version }}" + # Set override first to prevent rust-toolchain.toml from auto-installing + rustup override set ${{ inputs.rust-version }} 2>/dev/null || true + + # Check if we need to install/update the toolchain + if rustup toolchain list | grep -q "^${{ inputs.rust-version }}-"; then + rustup update ${{ inputs.rust-version }} + else + rustup toolchain install ${{ inputs.rust-version }} --profile minimal -c cargo,clippy,rustfmt + fi + else + echo "::group::📦 Setting up Rust from rust-toolchain.toml" + rustup show + fi + echo "::endgroup::" + + - name: Configure PATH and install tools + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + # Add .cargo/bin to PATH permanently for all subsequent steps + echo "${{ github.workspace }}/.cargo/bin" >> $GITHUB_PATH + + # For this step only, we need to add it to PATH since GITHUB_PATH takes effect in the next step + export PATH="${{ github.workspace }}/.cargo/bin:$PATH" + + # Install cargo-binstall for fast binary installations + if command -v cargo-binstall &> /dev/null; then + echo "✅ cargo-binstall already available" + else + echo "::group::📦 Installing cargo-binstall" + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + echo "::endgroup::" + fi + + if command -v prek &> /dev/null; then + echo "✅ prek already available" + else + echo "::group::📦 Installing prek" + # prek isn't regularly published to crates.io, so we use git source + cargo-binstall -y --no-symlinks --git https://github.com/j178/prek prek + echo "::endgroup::" + fi + + if command -v timelord &> /dev/null; then + echo "✅ timelord already available" + else + echo "::group::📦 Installing timelord" + cargo-binstall -y --no-symlinks timelord-cli + echo "::endgroup::" + fi + + - name: Configure sccache environment + shell: bash + run: | + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CUDA_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + + # Configure incremental compilation GC + # If we restored from old cache (partial hit), clean up aggressively + if [[ "${{ steps.build-cache.outputs.cache-hit }}" != "true" ]]; then + echo "♻️ Partial cache hit - enabling cache cleanup" + echo "CARGO_INCREMENTAL_GC_THRESHOLD=5" >> $GITHUB_ENV + fi + + - name: Install Rust components + if: inputs.rust-components != '' + shell: bash + run: | + echo "📦 Installing components: ${{ inputs.rust-components }}" + rustup component add ${{ inputs.rust-components }} + + - name: Install Rust target + if: inputs.rust-target != '' + shell: bash + run: | + echo "📦 Installing target: ${{ inputs.rust-target }}" + rustup target add ${{ inputs.rust-target }} + + - name: Output version and summary + id: rust-setup + shell: bash + run: | + RUST_VERSION=$(rustc --version | cut -d' ' -f2) + echo "version=$RUST_VERSION" >> $GITHUB_OUTPUT + + echo "📋 Setup complete:" + echo " Rust: $(rustc --version)" + echo " Cargo: $(cargo --version)" + echo " prek: $(prek --version 2>/dev/null || echo 'installed')" + echo " timelord: $(timelord --version 2>/dev/null || echo 'installed')" diff --git a/.forgejo/workflows/prek-checks.yml b/.forgejo/workflows/prek-checks.yml index ac330ca2..c25b9c3d 100644 --- a/.forgejo/workflows/prek-checks.yml +++ b/.forgejo/workflows/prek-checks.yml @@ -2,7 +2,6 @@ name: Checks / Prek on: push: - pull_request: permissions: contents: read @@ -17,18 +16,64 @@ jobs: with: persist-credentials: false - - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v5 + - name: Setup Rust nightly + uses: ./.forgejo/actions/setup-rust with: - enable-cache: true - ignore-nothing-to-cache: true - cache-dependency-glob: '' + rust-version: nightly + github-token: ${{ secrets.GH_PUBLIC_RO }} - name: Run prek run: | - uvx prek run \ + prek run \ --all-files \ --hook-stage manual \ --show-diff-on-failure \ --color=always \ -v + + - name: Check Rust formatting + run: | + cargo +nightly fmt --all -- --check && \ + echo "✅ Formatting check passed" || \ + exit 1 + + clippy-and-tests: + name: Clippy and Cargo Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup LLVM + uses: ./.forgejo/actions/setup-llvm-with-apt + with: + extra-packages: liburing-dev liburing2 + + - name: Setup Rust with caching + uses: ./.forgejo/actions/setup-rust + with: + github-token: ${{ secrets.GH_PUBLIC_RO }} + + - name: Run Clippy lints + run: | + cargo clippy \ + --workspace \ + --features full \ + --locked \ + --no-deps \ + --profile test \ + -- \ + -D warnings + + - name: Run Cargo tests + run: | + cargo test \ + --workspace \ + --features full \ + --locked \ + --profile test \ + --all-targets \ + --no-fail-fast diff --git a/.forgejo/workflows/rust-checks.yml b/.forgejo/workflows/rust-checks.yml deleted file mode 100644 index c46363a0..00000000 --- a/.forgejo/workflows/rust-checks.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Checks / Rust - -on: - push: - -jobs: - format: - name: Format - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install rust - uses: ./.forgejo/actions/rust-toolchain - with: - toolchain: "nightly" - components: "rustfmt" - - - name: Check formatting - run: | - cargo +nightly fmt --all -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install rust - uses: ./.forgejo/actions/rust-toolchain - - - uses: https://github.com/actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - github-api-url: https://api.github.com - owner: ${{ vars.GH_APP_OWNER }} - repositories: "" - - name: Install sccache - uses: ./.forgejo/actions/sccache - with: - token: ${{ steps.app-token.outputs.token }} - - run: sudo apt-get update - - name: Install system dependencies - uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1 - with: - packages: clang liburing-dev - version: 1 - - name: Cache Rust registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/git - !~/.cargo/git/checkouts - ~/.cargo/registry - !~/.cargo/registry/src - key: rust-registry-${{hashFiles('**/Cargo.lock') }} - - name: Timelord - uses: ./.forgejo/actions/timelord - with: - key: sccache-v0 - path: . - - name: Clippy - run: | - cargo clippy \ - --workspace \ - --features full \ - --locked \ - --no-deps \ - --profile test \ - -- \ - -D warnings - - - name: Show sccache stats - if: always() - run: sccache --show-stats - - cargo-test: - name: Cargo Test - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install rust - uses: ./.forgejo/actions/rust-toolchain - - - uses: https://github.com/actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - github-api-url: https://api.github.com - owner: ${{ vars.GH_APP_OWNER }} - repositories: "" - - name: Install sccache - uses: ./.forgejo/actions/sccache - with: - token: ${{ steps.app-token.outputs.token }} - - run: sudo apt-get update - - name: Install system dependencies - uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1 - with: - packages: clang liburing-dev - version: 1 - - name: Cache Rust registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/git - !~/.cargo/git/checkouts - ~/.cargo/registry - !~/.cargo/registry/src - key: rust-registry-${{hashFiles('**/Cargo.lock') }} - - name: Timelord - uses: ./.forgejo/actions/timelord - with: - key: sccache-v0 - path: . - - name: Cargo Test - run: | - cargo test \ - --workspace \ - --features full \ - --locked \ - --profile test \ - --all-targets \ - --no-fail-fast - - - name: Show sccache stats - if: always() - run: sccache --show-stats